Skip to content

对象转原始类型,会调用内置的 [ToPrimitive] 函数,对于该函数而言,其逻辑如下

  1. 如果 Symbol.toPrimitive () 方法,优先调用再返回
  2. 调用 valueOf (),如果转换为原始类型,则返回
  3. 调用 toString (),如果转换为原始类型,则返回
  4. 如果都没有返回原始类型,会报错

防抖

js
function debounce(func, wait = 50) {
  let timer
  return (...args) => {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}

节流

js
function throttle(func, wait) {
  let previous = 0
  return (...args) => {
    let now = Date.now()
    if (now - previous > wait) {
      func.apply(this, args)
      previous = now
    }
  }
}

EventLoop

概念

  1. 每个“线程”都有自己的事件循环,因此每个 Web Worker 都有自己的事件循环,因此它可以独立执行,而同一源上的所有窗口共享一个事件循环,因为它们可以同步通信。Tasks, microtasks, queues and schedules
  2. nestTick

浏览器端

  1. 一开始整段脚本作为第一个宏任务执行 =>
  2. 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列 =>
  3. 当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列(包括 v8 垃圾回收)为空 =>
  4. 执行浏览器 UI 线程的渲染工作 =>
  5. 检查是否有 Web worker 任务,有则执行
  6. 执行队首新的宏任务,回到 2,依此循环,直到宏任务和微任务队列都为空

宏任务包含

  1. script(整体代码)
  2. setTimeout
  3. setInterval
  4. I/O
  5. UI 交互事件(requestAnimationFrame)
  6. postMessage
  7. MessageChannel
  8. setImmediate(Node.js 环境)

微任务包括

  1. Promise.then
  2. Object.observe(已废弃)
  3. MutationObserver
  4. process.nextTick(Node.js 环境,优先级高于 Promise)
  5. v8 垃圾回收

队列说明

  1. 浏览器端,宏任务队列,微任务队列
  2. Nodejs 端,宏任务队列,setImmediate 队列(通常比 timer 队列慢执行),微任务队列,process.nextTick 微任务队列(优先普通微任务队列执行)

服务器端

  1. timer 阶段 Node.js 事件循环的发起点有 4 个: Node.js 启动后; setTimeout 回调函数; setInterval 回调函数;也可能是一次 I/O 后的回调函数。
  2. pending callbacks:本阶段执行某些系统操作(如 TCP 错误类型)的回调函数
  3. idle、prepare:仅系统内部使用,空闲、预备状态 (第 2 阶段结束,poll 未触发之前) 该阶段只供 libuv 内部调用
  4. poll 轮询 阶段: 检索新的 I/O 事件,执行与 I/O 相关的回调,在 node 代码中难免会有异步操作,比如文件 I/O,网络 I/O 等等,那么当这些异步操作做完了,就会来通知 JS 主线程,怎么通知呢?就是通过 'data'、'connect' 等事件使得事件循环到达 poll 阶段。到达了这个阶段后: 如果当前已经存在定时器,而且有定时器到时间了,拿出来执行,eventLoop 将回到 timer 阶段。如果没有则继续等待,相当于阻塞了一段时间 (阻塞时间是有上限的), 等待 callback 函数加入队列,加入后会立刻执行。一段时间后自动进入 check 阶段。
  5. check 阶段 setImmediate() 回调函数在这里执行,setImmediate 并不是立马执行,而是当事件循环 poll 中没有新的事件处理时就执行该部分
  6. close callbacks 关闭事件的回调阶段:执行一些关闭的回调函数,如 socket.on('close', ...)、socket.destroy ()。

::: primary 拓展阅读 多图生动详解浏览器与 Node 环境下的 Event Loop 上多图生动详解浏览器与 Node 环境下的 Event Loop 下 :::

实现继承

js
function Parent() {
  this.name = 'parent'
  this.play = [1, 2, 3]
}
function Child() {
  Parent5.call(this)
  this.type = 'child5'
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

手动实现一下 instanceof 的功能

js
function myInstanceof(left, right) {
  //基本数据类型直接返回false
  if (typeof left !== 'object' || left === null) return false
  //getPrototypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
  let proto = Object.getPrototypeOf(left)
  // 也可通过这个获取获得对象的原型
  // proto = left.__proto__
  while (true) {
    //查找到尽头,还没找到
    if (proto == null) return false
    //找到相同的原型对象
    if (proto == right.prototype) return true
    proto = Object.getPrototypeOf(proto)
    //proto = proto.__proto__
  }
}

new 被调用后做了四件事情

  1. 创建一个新对象
  2. 为这个新对象添加构造函数原型 (constructor.prototype) 所在原型链上的属性
  3. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
  4. 根据构造函数返回的结果判断返回引用类型还是值类型
javascript
function newOperator(ctor, ...args) {
  if (typeof ctor !== 'function') {
    throw 'newOperator function the first param must be a function'
  }
  // 链接到原型
  let obj = Object.create(ctor.prototype)
  //// 绑定 this,执行构造函数
  let res = ctor.apply(obj, args)

  let isObject = typeof res === 'object' && res !== null
  let isFunction = typeof res === 'function'
  return isObject || isFunction ? res : obj
}
function newOperator(ctor, ...args) {
  if (typeof ctor !== 'function') {
    throw 'the first param must be a function'
  }
  let obj = Object.create(ctor.prototype)
  let res = ctor.apply(obj, args)

  let isObject = typeof res === 'object' && res !== null
  let isFunction = typeof res === 'function'
  return isObject || isFunction ? res : obj
}

模拟实现一个 bind 的效果

  1. 对于普通函数,绑定 this 指向
  2. 对于构造函数,要保证原函数的原型对象上的属性不能丢失
js
Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  const _this = this
  const args = [...arguments].slice(1)
  // 返回一个函数
  return function F() {
    // 因为返回了一个函数,我们可以 new F(),所以需要判断
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat([...arguments]))
  }
}

Function.prototype.myBind = function(context){
  if(typeof context!=='function'){
    throw new TypeError('error')
  }
  const _this = this
  const args = [...arguments].slice(1)
  return function F(){
    if(this instanceof F){
      return new _this(...args,...arguments)
    }
    return _this.apply(context,args.concat([...arguments]))
}

模拟实现 call 函数

  1. 不传入第一个参数,那么上下文默认为 window
  2. 改变了 this 指向,让新的对象可以执行该函数,并能接受参数
js
Function.prototype.myCall = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  context = context || window
  context.fn = this
  const args = [...arguments].slice(1)
  const result = context.fn(...args)
  delete context.fn
  return result
}
Function.prototype.myCall = function(context, ...args) {
  if (typeof this !== 'function') {
    throw new TypeError('is not a function')
  }
  context = context || window
  context.fn = this
  const result = context.fn(...args)
  delete context.fn
  return result
}

模拟实现 apply 函数

  1. 与 call 区别只是参数不同
  2. 在参数少的情况下,call 的性能优于 apply,反之 apply 的性能更好。因此在执行回调时候可以根据情况调用 call 或者 apply。
js
Function.prototype.myApply = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  context = context || window
  context.fn = this
  let result
  // 处理参数和 call 有区别
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }
  delete context.fn
  return result
}
Function.prototype.myApply = function(context = window) {
  if (typeof this !== 'function') {
    throw new TypeError('error')
  }
  context.fn = this
  let args = arguments[1] || []
  const result = context.fn(...args)
  delete context.fn
  return result
}

实现对象深拷贝

二维数组扁平化

js
function flat1(arr) {
  return arr.flat(Infinity)
}
function flat2(arr) {
  return arr.reduce((prev, next) => prev.concat(Array.isArray(next) ? flat2(next) : next), [])
}
function flat3(arr){
  white(arr.some(Array.isArray)){
    arr = [].concat(...arr)
  }
  return arr
}

简易版及问题

js
JSON.parse(JSON.stringify())

// 1. 无法解决循环引用的问题。如拷贝 a 会出现系统栈溢出,因为出现了无限递归的情况。:
const a = { val: 2 }
a.target = a
// 无法拷贝一写特殊的对象,诸如 RegExp, Date, Set, Map 等。
// 无法拷贝函数 (划重点)。

详细版利用递归

js
function deepClone(obj) {
  // 如果是 值类型 或 null,则直接return
  if (typeof obj !== 'object' || obj === null) {
    return obj
  }

  // 定义结果对象
  let copy = {}

  // 如果对象是数组,则定义结果数组
  if (obj.constructor === Array) {
    copy = []
  }

  // 遍历对象的key
  for (let key in obj) {
    // 如果key是对象的自有属性
    if (obj.hasOwnProperty(key)) {
      // 递归调用深拷贝方法
      copy[key] = deepClone(obj[key])
    }
  }

  return copy
}
function deepClone(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj
  }
  let copy = {}
  if (obj.constructor === Array) {
    copy = []
  }
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepClone(obj[key])
    }
  }
  return copy
}

函数与箭头函数

  1. 在 JS 种有两种函数,一种是普通函数,另一种是箭头函数。每个普通函数都是 Function 的实例,而箭头函数不是任何类的实例,每次调用都是不一样的引用。那么如何来区分两者呢?答案是:利用原型。箭头函数是不存在原型的。

Promise

Promise 利用了三大技术手段来解决回调地狱

  1. 回调函数延迟绑定。
  2. 返回值穿透。
  3. 错误冒泡。

为什么 Promise 要引入微任务?

  1. 如何处理回调的问题。总结起来有三种方式:
    1. 使用同步回调,直到异步任务进行完,再进行后面的任务。
    2. 使用异步回调,将回调函数放在进行宏任务队列的队尾。
    3. 使用异步回调,将回调函数放到当前宏任务中的最后面。
  2. 第一种方式显然不可取,因为同步的问题非常明显,会让整个脚本阻塞住,当前任务等待,后面的任务都无法得到执行,而这部分等待的时间是可以拿来完成其他事情的,导致 CPU 的利用率非常低,而且还有另外一个致命的问题,就是无法实现延迟绑定的效果。如果采用第二种方式,那么执行回调 (resolve/reject) 的时机应该是在前面所有的宏任务完成之后,倘若现在的任务队列非常长,那么回调迟迟得不到执行,造成应用卡顿。为了解决上述方案的问题,另外也考虑到延迟绑定的需求,Promise 采取第三种方式,即引入微任务 , 即把 resolve (reject) 回调的执行放在当前宏任务的末尾。这样
  3. 利用微任务解决了两大痛点:
    1. 采用异步回调替代同步回调解决了浪费 CPU 性能的问题。
    2. 放到当前宏任务最后执行,解决了回调执行的实时性问题。

简易版 Promise

js
// 三个常量用于表示状态
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

function MyPromise(fn) {
  const that = this
  this.state = PENDING

  // value 变量用于保存 resolve 或者 reject 中传入的值
  this.value = null

  // 用于保存 then 中的回调,因为当执行完 Promise 时状态可能还是等待中,这时候应该把 then 中的回调保存起来用于状态改变时使用
  that.resolvedCallbacks = []
  that.rejectedCallbacks = []

  function resolve(value) {
    // 首先两个函数都得判断当前状态是否为等待中
    if (that.state === PENDING) {
      that.state = RESOLVED
      that.value = value

      // 遍历回调数组并执行
      that.resolvedCallbacks.map(cb => cb(that.value))
    }
  }
  function reject(value) {
    if (that.state === PENDING) {
      that.state = REJECTED
      that.value = value
      that.rejectedCallbacks.map(cb => cb(that.value))
    }
  }

  // 完成以上两个函数以后,我们就该实现如何执行 Promise 中传入的函数了
  try {
    fn(resolve, reject)
  } catch (e) {
    reject(e)
  }
}

// 最后我们来实现较为复杂的 then 函数
MyPromise.prototype.then = function(onFulfilled, onRejected) {
  const that = this

  // 判断两个参数是否为函数类型,因为这两个参数是可选参数
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
  onRejected = typeof onRejected === 'function' ? onRejected : e => throw e

  // 当状态不是等待态时,就去执行相对应的函数。如果状态是等待态的话,就往回调函数中 push 函数
  if (this.state === PENDING) {
    this.resolvedCallbacks.push(onFulfilled)
    this.rejectedCallbacks.push(onRejected)
  }
  if (this.state === RESOLVED) {
    onFulfilled(that.value)
  }
  if (this.state === REJECTED) {
    onRejected(that.value)
  }
  return this
}

const PENDING = 'pending'
const RESOLVE = 'resolve'
const REJECT = 'reject'
function Promise(fn) {
  const _this = this
  this.state = PENDING
  this.value = null
  this.resolveCallbacks = []
  this.rejectCallbacks = []
  function resolve(value) {
    if (_this.state === PENDING) {
      _this.state = RESOLVE
      _this.value = value
      _this.resolvedCallbacks.map(cb => cb(_this.value))
    }
  }
  function reject(value) {
    if (_this.state === PENDING) {
      _this.state = REJECT
      _this.value = value
      _this.rejectCallbacks.map(cb => cb(_this.value))
    }
  }
  try {
    fn(resolve, reject)
  } catch (e) {
    reject(e)
  }
}
Promise.prototype.then = function(onFulfilled, onRejected) {
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
  onRejected = typeof onRejected === 'function' ? onRejected : e => throw e
  if (this.state === PENDING) {
    this.resolveCallbacks.push(onFulfilled)
    this.rejectCallbacks.push(onRejected)
  }
  if (this.state === RESOLVE) {
    onFulfilled(this.value)
  }
  if (this.state === REJECT) {
    onRejected(this.value)
  }
  return this
}

实现 Promise.all

js
Promise.all = function(promises) {
  return new Promise((resolve, reject) => {
    let result = []
    let index = 0
    let len = promises.length
    if (len === 0) {
      resolve(result)
      return
    }

    for (let i = 0; i < len; i++) {
      // 为什么不直接 promise[i].then, 因为promise[i]可能不是一个promise
      Promise.resolve(promise[i])
        .then(data => {
          result[i] = data
          index++
          if (index === len) resolve(result)
        })
        .catch(err => {
          reject(err)
        })
    }
  })
}

实现 Promise.race

js
Promise.race = function(promises) {
  return new Promise((resolve, reject) => {
    let len = promises.length
    if (len === 0) return
    for (let i = 0; i < len; i++) {
      Promise.resolve(promise[i])
        .then(data => {
          resolve(data)
          return
        })
        .catch(err => {
          reject(err)
          return
        })
    }
  })
}

Promise 并发控制

题目参数

js
let urls =  ['bytedance.com','tencent.com','alibaba.com','microsoft.com','apple.com','hulu.com','amazon.com'] // 请求地址

//自定义请求函数
const request = url => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(`任务${url}完成`)
        }, 1000)
    }).then(res => {
        console.log('外部逻辑', res);
    })
}
  1. 简易递归版本
js
const run = (urls, limit = 3) => {
  const pool = []

  const addTask = () => {
    const url = request(urls.shift())
    url.finally(() => {
      const idx = pool.indexOf(url)
      pool.splice(idx, 1)
      if(urls.length) {
        addTask()
      }
    })

    pool.push(url)
  }
  
  // 首次调用填满并发池
  while (pool.length < limit) {
    addTask()
  }
}

run(urls, 3)
  1. 利用 Promise.race()
JavaScript
//添加任务
function addTask(url){
    let task = request(url);
    pool.push(task);
    task.then(res => {
        //请求结束后将该Promise任务从并发池中移除
        pool.splice(pool.indexOf(task), 1);
        console.log(`${url} 结束,当前并发数:${pool.length}`);
    })
}
//每当并发池跑完一个任务,就再塞入一个任务
function run(race){
    race.then(res => {
        let url = urls.shift();
        if(!url) return '请求完成'
        // 还有任务,继续请求
        addTask(url);
        run(Promise.race(pool));
    })
}

let pool = []//并发池
let max = 3 //最大并发量
//先循环把并发池塞满
while (pool.length < max) {
    let url = urls.shift();
    addTask(url)
}
//利用Promise.race方法来获得并发池中某任务完成的信号
let race = Promise.race(pool)
run(race)

拓展:通过 Iterator 控制 Promise.all 的并发数

forEach 中用 await

  1. 对于异步代码,forEach 并不能保证按顺序执行。 forEach 底层使用的 for...in...直接遍历进行回调导致它无法保证异步任务的执行顺序。比如后面的任务用时短,那么就又可能抢在前面的任务之前执行。
  2. for...of for...of 并不像 forEach 那么简单粗暴的方式去遍历执行,而是采用一种特别的手段 —— 迭代器去遍历。
  3. 原生具有 [Symbol.iterator] 属性数据类型为可迭代数据类型。如数组、类数组(如 arguments、NodeList)、Set 和 Map。
js
let arr = [4, 2, 1]
// 这就是迭代器
let iterator = arr[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

// {value: 4, done: false}
// {value: 2, done: false}
// {value: 1, done: false}
// {value: undefined, done: true}

//斐波那契数列 (50 以内)
function* fibonacci() {
  let [prev, cur] = [0, 1]
  console.log(cur)
  while (true) {
    ;[prev, cur] = [cur, prev + cur]
    yield cur
  }
}

for (let item of fibonacci()) {
  if (item > 50) break
  console.log(item)
}
// 1
// 1
// 2
// 3
// 5
// 8
// 13
// 21
// 34

ES5 去写一个能够生成迭代器对象的迭代器生成函数

js
// 定义生成器函数,入参是任意集合
function iteratorGenerator(list) {
  // idx记录当前访问的索引
  var idx = 0
  // len记录传入集合的长度
  var len = list.length
  return {
    // 自定义next方法
    next: function() {
      // 如果索引还没有超出集合长度,done为false
      var done = idx >= len
      // 如果done为false,则可以继续取值
      var value = !done ? list[idx++] : undefined

      // 将当前值与遍历是否完毕(done)返回
      return {
        done: done,
        value: value,
      }
    },
  }
}

var iterator = iteratorGenerator(['1号选手', '2号选手', '3号选手'])
iterator.next()
iterator.next()
iterator.next()

react-thunk

js
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => Action => {
    if (typeof Action === 'function') {
      return Action(dispatch, getState, extraArgument)
    }
    return next(Action)
  }
}

const thunk = createThunkMiddleware()

thunk.withExtraArgument = createThunkMiddleware

export default thunk

DOM

获取页面所有标签名称

const set = new Set()
function collectDom(el){
    for(const item of el.children){
        set.add(item.tagName)
        collect(item)
    }
}
collect(document.documentElement)
console.log(set)

原生 API 实现

map

Array.prototype.MyMap = function(fn, context){
  let arr = Array.prototype.slice.call(this);//由于是ES5所以就不用...展开符了
  let mappedArr = [];
  for (let i = 0; i < arr.length; i++ ){
    mappedArr.push(fn.call(context, arr[i], i, this));
  }
  return mappedArr;
}

reduce

Array.prototype.myReduce = function(fn, initialValue) {
  let arr = Array.prototype.slice.call(this);
  let res, startIndex;
  res = initialValue ? initialValue : arr[0];
  startIndex = initialValue ? 0 : 1;
  for(let i = startIndex; i < arr.length; i++) {
    res = fn.call(null, res, arr[i], i, this);
  }
  return res;
}

TS 类型体操实现 TrimLeft

typescript
type TrimLeft<Str extends string> = Str extends `${' '}${infer Rest}` ? TrimLeft<Rest> : Str
type res = TrimLeft<'   abc'> //'abc'

::: primary 拓展阅读 TS 类型体操 :::