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 ()。

实现继承 ​

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)
  2. 利用 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)

拓展:

  1. 通过 Iterator 控制 Promise.all 的并发数
  2. 利用 Async 和 Await 创建一个并发任务队列

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 ​

获取页面所有标签名称 ​

js
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 ​

js
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 ​

js
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'

拓展阅读

TS 类型体操