面试系列——经典手写源码(一)

575 阅读5分钟

正值金九银十之际,小伙伴们纷纷摩拳擦掌,准备升值加薪,赢取白富美,走上人生巅峰!

这篇文章虽然和上面关系不大,但至少可以当做摩拳擦掌用的润滑剂,希望能助大家一臂之力。

在前端的面试中,有些面试官会考查候选人的手撕代码能力,往往会选择让候选人手动实现某些原生JavaScript方法,下面则手动实现了一些常见的API。

废话不多说,直接上正文。

Promise

前不久,前同事面试某公司时被问到了这个问题,Promise的实现也是各个手写题中出现频率较高的题目。

Promise有三个状态

  • pending
  • fulfilled
  • rejected

一旦Promise实例的状态从 pending 转为 fulfilled 或者 rejected,则不能逆转。

  • 初始状态是pending
  • 同一个Promise实例可以多次调用then方法
  • then方法如果有返回promise实例,下面的链式调用会沿用这个返回的promise
  • then方法传递的回调函数一定在下个事件循环中执行,且先于宏任务执行,无论是否异步调用resolve

直接上代码

function Promise (func) {
  this.state = 'pending'
  this.callbacks = [] // 存储回调
  this.data = '' // 存储value或error
  const resolve = (value) => {
    const fn = () => {
      if (value && value instanceof Promise) {
      	// value是Promise实例时,则先链式调用,并把当然resolve当做resolveFn传递下去
        value.then(resolve, reject)
        return
      }
      // Todo
      this.state = 'fulfilled'
      this.data = value
      for (let callback of this.callbacks) {
        this.__dispatch(callback)
      }
      this.callbacks = []
    }
    getMicroTask(fn) // 临时造了个微任务
  }

  const reject = err => {
    const fn = () => {
      this.state = 'rejected'
      this.data = err
      for (let callback of this.callbacks) {
        this.__dispatch(callback)
      }
      this.callbacks = []
    }
    getMicroTask(fn)
  }
  func(resolve, reject)
}

Promise.prototype.then = function (resolveFn, rejectFn) {
  return new Promise((resolve, reject) => {
    this.__dispatch({
      resolveFn,
      rejectFn,
      resolve,
      reject
    })
  })
}

Promise.prototype.__dispatch = function (callback) {
  // 私有方法,用来处理callback
  if (this.state === 'pending') {
  	// 如果是待定状态,则加入callback中
    this.callbacks.push(callback)
    return
  }
  const { resolveFn, rejectFn, resolve, reject } = callback
  if (!resolveFn && !rejectFn) {
  	// 没有传任何回调
    this.state === 'fulfilled' ? resolve(this.data) : reject(this.data)
    return
  }
  let res
  try {
    res = this.state === 'fulfilled' ? resolveFn(this.data) : rejectFn(this.data)
  } catch (err) {
    reject(err)
    return
  }
  resolve(res)
}

function getMicroTask(handler) {
	// 救急用,当做微任务,因为没了Promise的微任务……
    let counter = 1
    let observer = new MutationObserver(handler)
   	let textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
    	characterData: true
    })
    counter = (counter + 1) % 2
    textNode.data = String(counter)
}
  • callbacks 的元素并不是函数,而是一个存储着resolveFn,rejectFn,then方法默认返回的Promise中的resolve及reject的对象
  • __dispatch 方法在 pending 状态时用于存储回调函数,而在状态发生改变时,用来直接触发回调
  • 该实现仅仅用来说明Promise的原理,并适用于生产环境

是骡子是马,拉出来溜溜

new Promise(resolve => {
  setTimeout(resolve, 500, 10)
}).then(value => {
  console.log(value)
  return new Promise((resolve, reject) => {
    setTimeout(reject, 500, 'error')
  })
}).then(value => {
  console.log(value)
}, err => {
  console.log(err)
})
// 10
// error

上面的例子看起来没问题,具体是否有遗漏还需要工具去测试

还有 catchfinally

catch

Promise.prototype.catch = function(rejectFn) {
	return this.then(null, rejectFn)
}

finally

Promise.prototype.finally = function(finallyFn) {
	return this.then(finallyFn, finallyFn)
}

catchfinally 方法很简单,无非就是 then 方法的变体

Promise 还提供了一些其他的API

Promise.resolve

Promise.resolve 方法返回一个 Promise 实例,其状态为 fulfilled

Promise.resolve = function (value) {
	if (!value) {
    	// value是空值情况
    	return new Promise(resolve => resolve())
    }
    if (value instanceof Promise) {
    	// value本事就是个Promise实例
    	return value
    }
    return new Promise(resolve => resolve(value))
}

Promise.reject

Promise.reject 返回一个 Promise 实例,其状态为 rejected,但是它不会对传入的值做处理

Promise.reject = function(error) {
	return new Promise((resolve, reject) => reject(error))
}

Promise.all

Promise.all 将一系列以数组存储的 Promise 实例包装成新的实例,并将所有实例返回的值以数组的形式传出来

  • 成功返回的数组按照 promise 实例的顺序输出
  • 如果有报错,则中断执行,并返回第一个报错的 promise 实例
Promise.all = function(fns) {
	let count = 0
    let result = Array.from({length: fns.length}, () => null)
    return new Promise((resolve, reject) => {
    	if (!fns || !fns.length) {
        	resolve(result)
        } else {
        	for (let i = 0, len = fns.length; i < len; ++i) {
            	fns[i].then(res => {
                	result[i] = res
                    count++
                    if (count === fns.length) {
                    	resolve(result)
                        return
                    }
                }).catch(err => {
                	reject(err)
                    return
                })
            }
        }
    })
}

至此,Promise系列基本写完了,相信应付面试应该没问题了

下面继续其他常见的手写系列

call, apply, bind

callapplybind 并称手写源码系列三幻神,我们当然也不可或缺

  • callapply 的实现基本一致,只不过是传参的形式不一样
  • bind 的实现稍稍麻烦点,会考虑 new 调用的情况

call

Function.prototype.call = function (context, ...args) {
    context = context || window // 默认window对象
    const cxtObj = Object.create(context)
    cxtObj.fn = this
    return cxtObj.fn(...args)
}

apply

Function.prototype.apply = function (context, args) {
    context = context || window
    const cxtObj = Object.create(context)
    cxtObj.fn = this
    return cxtObj.fn(...args)
}

bind

Function.prototype.bind = function (context, ...args) {
	context = context || window
    const fn = this
    const bindFn = function (...bindArg) {
    	// 如果是new调用,则this指向new操作符创建出来的对象
    	return fn.apply(this instanceof bindFn ? this : context, [...args, ...bindArg])
    }
    // 不要忘记原型链
    bindFn.prototype = Object.create(fn.prototype)
    return bindFn
}

三幻神基本完成,基本比较简单,注意下细节即可,注意下callapply的区别和bind返回函数的 new 调用。

indexOf

indexOf 方法如果使用常规的暴力字符串匹配会很简单,但是我们这里不会使用暴力的方法,我们使用 KMP 的算法来实现,不懂 KMP 的可以百度一下。

function indexOf(source, pattern) {
	// kmp算法的精髓在这个next数组的计算上,我们会先计算next数组
    // next数组的每一项代表pattern[当前索引]的最长可匹配前缀字符串最后一个字符的下标
    // 运用了DP的思想ababa
    // 假设patten === 'abcabc', 当j = 4时,此时的最长可匹配前缀子串是ab, 对应的最后一个字符的下标是1
     // 即map[4] = 1, 那么j++后,j==5时,此时,如果map[4] 对应的子串ab 下一个 pattern[2] === pattern[5]时,则map[5] = 2
	const next = Array.from({length: pattern.length}, () => -1)
    let k = -1
    // 先遍历要查找的模式串,用来生成next数组
    for (let i = 1; i < pattern.length; ++i) {
    	while (k !== -1 && pattern[k+1] !== pattern[i]) {
        	// abadabab 当i = 7时,k = 2, 此时pattern[k+1] = 'd'
            // 而pattern[i] = 'b',两者不想等,此时让k退化至next[k],即 0 处,此时pattern[k+1] === pattern[i]
        	k = next[k]
        }
        if (pattern[k+1] === pattern[i]) {
        	k++
        }
        next[i] = k
    }
    let j = 0
    for (let i = 0; i < source.length; ++i) {
    	while (j > 0 && source[i] !== pattern[j]) {
        	// 碰到不一致则将pattern字符串右移,移动多少位根据next来
        	j = next[j-1] + 1
        }
        if (source[i] === pattern[j]) {
        	j++
        }
        if (j === pattern.length) {
        	return i - j + 1
        }
    }
    return -1
}

KMP算法不太好懂,不懂的同学直接用暴力匹配即可

结束

这篇文章先写到这里吧,虽然短短的几个源码实现,但其实考查的东西涉及到很多,另外,学会了如何实现,那么如何使用这些方法就简单很多啦,也能帮助大家少踩一些使用上的坑!

参考