正值金九银十之际,小伙伴们纷纷摩拳擦掌,准备升值加薪,赢取白富美,走上人生巅峰!
这篇文章虽然和上面关系不大,但至少可以当做摩拳擦掌用的润滑剂,希望能助大家一臂之力。
在前端的面试中,有些面试官会考查候选人的手撕代码能力,往往会选择让候选人手动实现某些原生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
上面的例子看起来没问题,具体是否有遗漏还需要工具去测试
还有 catch 和 finally
catch
Promise.prototype.catch = function(rejectFn) {
return this.then(null, rejectFn)
}
finally
Promise.prototype.finally = function(finallyFn) {
return this.then(finallyFn, finallyFn)
}
catch 和 finally 方法很简单,无非就是 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
call、apply 及 bind 并称手写源码系列三幻神,我们当然也不可或缺
call和apply的实现基本一致,只不过是传参的形式不一样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
}
三幻神基本完成,基本比较简单,注意下细节即可,注意下call和apply的区别和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算法不太好懂,不懂的同学直接用暴力匹配即可
结束
这篇文章先写到这里吧,虽然短短的几个源码实现,但其实考查的东西涉及到很多,另外,学会了如何实现,那么如何使用这些方法就简单很多啦,也能帮助大家少踩一些使用上的坑!