死磕 36 個(gè) JS 手寫(xiě)題(搞懂后,提升真的大)
這是布蘭的第 22 篇原創(chuàng)
為什么要寫(xiě)這類(lèi)文章
作為一個(gè)程序員,代碼能力毋庸置疑是非常非常重要的,就像現(xiàn)在為什么大廠面試基本都問(wèn)什么 API 怎么實(shí)現(xiàn)可見(jiàn)其重要性。我想說(shuō)的是居然手寫(xiě)這么重要,那我們就必須掌握它,所以文章標(biāo)題用了死磕,一點(diǎn)也不過(guò)分,也希望不被認(rèn)為是標(biāo)題黨。
作為一個(gè)普通前端,我是真的寫(xiě)不出 Promise A+ 規(guī)范,但是沒(méi)關(guān)系,我們可以站在巨人的肩膀上,要相信我們現(xiàn)在要走的路,前人都走過(guò),所以可以找找現(xiàn)在社區(qū)已經(jīng)存在的那些優(yōu)秀的文章,比如工業(yè)聚大佬寫(xiě)的 100 行代碼實(shí)現(xiàn) Promises/A+ 規(guī)范,找到這些文章后不是收藏夾吃灰,得找個(gè)時(shí)間踏踏實(shí)實(shí)的學(xué),一行一行的磨,直到搞懂為止。我現(xiàn)在就是這么干的。
能收獲什么
這篇文章總體上分為 2 類(lèi)手寫(xiě)題,前半部分可以歸納為是常見(jiàn)需求,后半部分則是對(duì)現(xiàn)有技術(shù)的實(shí)現(xiàn);
對(duì)常用的需求進(jìn)行手寫(xiě)實(shí)現(xiàn),比如數(shù)據(jù)類(lèi)型判斷函數(shù)、深拷貝等可以直接用于往后的項(xiàng)目中,提高了項(xiàng)目開(kāi)發(fā)效率; 對(duì)現(xiàn)有關(guān)鍵字和 API 的實(shí)現(xiàn),可能需要用到別的知識(shí)或 API,比如在寫(xiě) forEach 的時(shí)候用到了無(wú)符號(hào)位右移的操作,平時(shí)都不怎么能夠接觸到這玩意,現(xiàn)在遇到了就可以順手把它掌握了。所以手寫(xiě)這些實(shí)現(xiàn)能夠潛移默化的擴(kuò)展并鞏固自己的 JS 基礎(chǔ); 通過(guò)寫(xiě)各種測(cè)試用例,你會(huì)知道各種 API 的邊界情況,比如 Promise.all, 你得考慮到傳入?yún)?shù)的各種情況,從而加深了對(duì)它們的理解及使用;
閱讀的時(shí)候需要做什么
閱讀的時(shí)候,你需要把每行代碼都看懂,知道它在干什么,為什么要這么寫(xiě),能寫(xiě)得更好嘛?比如在寫(xiě)圖片懶加載的時(shí)候,一般我們都是根據(jù)當(dāng)前元素的位置和視口進(jìn)行判斷是否要加載這張圖片,普通程序員寫(xiě)到這就差不多完成了。而大佬程序員則是會(huì)多考慮一些細(xì)節(jié)的東西,比如性能如何更優(yōu)?代碼如何更精簡(jiǎn)?比如 yeyan1996 寫(xiě)的圖片懶加載就多考慮了 2 點(diǎn):比如圖片全部加載完成的時(shí)候得把事件監(jiān)聽(tīng)給移除;比如加載完一張圖片的時(shí)候,得把當(dāng)前 img 從 imgList 里移除,起到優(yōu)化內(nèi)存的作用。
除了讀通代碼之外,還可以打開(kāi) Chrome 的 Script snippet 去寫(xiě)測(cè)試用例跑跑代碼,做到更好的理解以及使用。
在看了幾篇以及寫(xiě)了很多測(cè)試用例的前提下,嘗試自己手寫(xiě)實(shí)現(xiàn),看看自己到底掌握了多少。條條大路通羅馬,你還能有別的方式實(shí)現(xiàn)嘛?或者你能寫(xiě)得比別人更好嘛?
好了,還楞著干啥,開(kāi)始干活。
數(shù)據(jù)類(lèi)型判斷
typeof 可以正確識(shí)別:Undefined、Boolean、Number、String、Symbol、Function 等類(lèi)型的數(shù)據(jù),但是對(duì)于其他的都會(huì)認(rèn)為是 object,比如 Null、Date 等,所以通過(guò) typeof 來(lái)判斷數(shù)據(jù)類(lèi)型會(huì)不準(zhǔn)確。但是可以使用 Object.prototype.toString 實(shí)現(xiàn)。
function typeOf(obj) {
let res = Object.prototype.toString.call(obj).split(' ')[1]
res = res.substring(0, res.length - 1).toLowerCase()
return res
}
typeOf([]) // 'array'
typeOf({}) // 'object'
typeOf(new Date) // 'date'
繼承
原型鏈繼承
function Animal() {
this.colors = ['black', 'white']
}
Animal.prototype.getColor = function() {
return this.colors
}
function Dog() {}
Dog.prototype = new Animal()
let dog1 = new Dog()
dog1.colors.push('brown')
let dog2 = new Dog()
console.log(dog2.colors) // ['black', 'white', 'brown']
原型鏈繼承存在的問(wèn)題:
問(wèn)題1:原型中包含的引用類(lèi)型屬性將被所有實(shí)例共享; 問(wèn)題2:子類(lèi)在實(shí)例化的時(shí)候不能給父類(lèi)構(gòu)造函數(shù)傳參;
借用構(gòu)造函數(shù)實(shí)現(xiàn)繼承
function Animal(name) {
this.name = name
this.getName = function() {
return this.name
}
}
function Dog(name) {
Animal.call(this, name)
}
Dog.prototype = new Animal()
借用構(gòu)造函數(shù)實(shí)現(xiàn)繼承解決了原型鏈繼承的 2 個(gè)問(wèn)題:引用類(lèi)型共享問(wèn)題以及傳參問(wèn)題。但是由于方法必須定義在構(gòu)造函數(shù)中,所以會(huì)導(dǎo)致每次創(chuàng)建子類(lèi)實(shí)例都會(huì)創(chuàng)建一遍方法。
組合繼承
組合繼承結(jié)合了原型鏈和盜用構(gòu)造函數(shù),將兩者的優(yōu)點(diǎn)集中了起來(lái)。基本的思路是使用原型鏈繼承原型上的屬性和方法,而通過(guò)盜用構(gòu)造函數(shù)繼承實(shí)例屬性。這樣既可以把方法定義在原型上以實(shí)現(xiàn)重用,又可以讓每個(gè)實(shí)例都有自己的屬性。
function Animal(name) {
this.name = name
this.colors = ['black', 'white']
}
Animal.prototype.getName = function() {
return this.name
}
function Dog(name, age) {
Animal.call(this, name)
this.age = age
}
Dog.prototype = new Animal()
Dog.prototype.constructor = Dog
let dog1 = new Dog('奶昔', 2)
dog1.colors.push('brown')
let dog2 = new Dog('哈赤', 1)
console.log(dog2)
// { name: "哈赤", colors: ["black", "white"], age: 1 }
寄生式組合繼承
組合繼承已經(jīng)相對(duì)完善了,但還是存在問(wèn)題,它的問(wèn)題就是調(diào)用了 2 次父類(lèi)構(gòu)造函數(shù),第一次是在 new Animal(),第二次是在 Animal.call() 這里。
所以解決方案就是不直接調(diào)用父類(lèi)構(gòu)造函數(shù)給子類(lèi)原型賦值,而是通過(guò)創(chuàng)建空函數(shù) F 獲取父類(lèi)原型的副本。
寄生式組合繼承寫(xiě)法上和組合繼承基本類(lèi)似,區(qū)別是如下這里:
- Dog.prototype = new Animal()
- Dog.prototype.constructor = Dog
+ function F() {}
+ F.prototype = Animal.prototype
+ let f = new F()
+ f.constructor = Dog
+ Dog.prototype = f
稍微封裝下上面添加的代碼后:
function object(o) {
function F() {}
F.prototype = o
return new F()
}
function inheritPrototype(child, parent) {
let prototype = object(parent.prototype)
prototype.constructor = child
child.prototype = prototype
}
inheritPrototype(Dog, Animal)
如果你嫌棄上面的代碼太多了,還可以基于組合繼承的代碼改成最簡(jiǎn)單的寄生式組合繼承:
- Dog.prototype = new Animal()
- Dog.prototype.constructor = Dog
+ Dog.prototype = Object.create(Animal.prototype)
+ Dog.prototype.constructor = Dog
class 實(shí)現(xiàn)繼承
class Animal {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Dog extends Animal {
constructor(name, age) {
super(name)
this.age = age
}
}
數(shù)組去重
ES5 實(shí)現(xiàn):
function unique(arr) {
var res = arr.filter(function(item, index, array) {
return array.indexOf(item) === index
})
return res
}
ES6 實(shí)現(xiàn):
var unique = arr => [...new Set(arr)]
數(shù)組扁平化
數(shù)組扁平化就是將 [1, [2, [3]]] 這種多層的數(shù)組拍平成一層 [1, 2, 3]。使用 Array.prototype.flat 可以直接將多層數(shù)組拍平成一層:
[1, [2, [3]]].flat(2) // [1, 2, 3]
現(xiàn)在就是要實(shí)現(xiàn) flat 這種效果。
ES5 實(shí)現(xiàn):遞歸。
function flatten(arr) {
var result = [];
for (var i = 0, len = arr.length; i < len; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]))
} else {
result.push(arr[i])
}
}
return result;
}
ES6 實(shí)現(xiàn):
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
深淺拷貝
淺拷貝:只考慮對(duì)象類(lèi)型。
function shallowCopy(obj) {
if (typeof obj !== 'object') return
let newObj = obj instanceof Array ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key]
}
}
return newObj
}
簡(jiǎn)單版深拷貝:只考慮普通對(duì)象屬性,不考慮內(nèi)置對(duì)象和函數(shù)。
function deepClone(obj) {
if (typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
}
}
return newObj;
}
復(fù)雜版深克隆:基于簡(jiǎn)單版的基礎(chǔ)上,還考慮了內(nèi)置對(duì)象比如 Date、RegExp 等對(duì)象和函數(shù)以及解決了循環(huán)引用的問(wèn)題。
const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;
function deepClone(target, map = new WeakMap()) {
if (map.get(target)) {
return target;
}
// 獲取當(dāng)前值的構(gòu)造函數(shù):獲取它的類(lèi)型
let constructor = target.constructor;
// 檢測(cè)當(dāng)前對(duì)象target是否與正則、日期格式對(duì)象匹配
if (/^(RegExp|Date)$/i.test(constructor.name)) {
// 創(chuàng)建一個(gè)新的特殊對(duì)象(正則類(lèi)/日期類(lèi))的實(shí)例
return new constructor(target);
}
if (isObject(target)) {
map.set(target, true); // 為循環(huán)引用的對(duì)象做標(biāo)記
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop], map);
}
}
return cloneTarget;
} else {
return target;
}
}
事件總線(xiàn)(發(fā)布訂閱模式)
class EventEmitter {
constructor() {
this.cache = {}
}
on(name, fn) {
if (this.cache[name]) {
this.cache[name].push(fn)
} else {
this.cache[name] = [fn]
}
}
off(name, fn) {
let tasks = this.cache[name]
if (tasks) {
const index = tasks.findIndex(f => f === fn || f.callback === fn)
if (index >= 0) {
tasks.splice(index, 1)
}
}
}
emit(name, once = false, ...args) {
if (this.cache[name]) {
// 創(chuàng)建副本,如果回調(diào)函數(shù)內(nèi)繼續(xù)注冊(cè)相同事件,會(huì)造成死循環(huán)
let tasks = this.cache[name].slice()
for (let fn of tasks) {
fn(...args)
}
if (once) {
delete this.cache[name]
}
}
}
}
// 測(cè)試
let eventBus = new EventEmitter()
let fn1 = function(name, age) {
console.log(`${name} ${age}`)
}
let fn2 = function(name, age) {
console.log(`hello, ${name} ${age}`)
}
eventBus.on('aaa', fn1)
eventBus.on('aaa', fn2)
eventBus.emit('aaa', false, '布蘭', 12)
// '布蘭 12'
// 'hello, 布蘭 12'
解析 URL 參數(shù)為對(duì)象
function parseParam(url) {
const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 將 ? 后面的字符串取出來(lái)
const paramsArr = paramsStr.split('&'); // 將字符串以 & 分割后存到數(shù)組中
let paramsObj = {};
// 將 params 存到對(duì)象中
paramsArr.forEach(param => {
if (/=/.test(param)) { // 處理有 value 的參數(shù)
let [key, val] = param.split('='); // 分割 key 和 value
val = decodeURIComponent(val); // 解碼
val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判斷是否轉(zhuǎn)為數(shù)字
if (paramsObj.hasOwnProperty(key)) { // 如果對(duì)象有 key,則添加一個(gè)值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 如果對(duì)象沒(méi)有這個(gè) key,創(chuàng)建 key 并設(shè)置值
paramsObj[key] = val;
}
} else { // 處理沒(méi)有 value 的參數(shù)
paramsObj[param] = true;
}
})
return paramsObj;
}
字符串模板
function render(template, data) {
const reg = /\{\{(\w+)\}\}/; // 模板字符串正則
if (reg.test(template)) { // 判斷模板里是否有模板字符串
const name = reg.exec(template)[1]; // 查找當(dāng)前模板里第一個(gè)模板字符串的字段
template = template.replace(reg, data[name]); // 將第一個(gè)模板字符串渲染
return render(template, data); // 遞歸的渲染并返回渲染后的結(jié)構(gòu)
}
return template; // 如果模板沒(méi)有模板字符串直接返回
}
測(cè)試:
let template = '我是{{name}},年齡{{age}},性別{{sex}}';
let person = {
name: '布蘭',
age: 12
}
render(template, person); // 我是布蘭,年齡12,性別undefined
圖片懶加載
與普通的圖片懶加載不同,如下這個(gè)多做了 2 個(gè)精心處理:
圖片全部加載完成后移除事件監(jiān)聽(tīng); 加載完的圖片,從 imgList 移除;
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length
const imgLazyLoad = function() {
let count = 0
return function() {
let deleteIndexList = []
imgList.forEach((img, index) => {
let rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
img.src = img.dataset.src
deleteIndexList.push(index)
count++
if (count === length) {
document.removeEventListener('scroll', imgLazyLoad)
}
}
})
imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
}
}
// 這里最好加上防抖處理
document.addEventListener('scroll', imgLazyLoad)
參考:圖片懶加載[1]
函數(shù)防抖
觸發(fā)高頻事件 N 秒后只會(huì)執(zhí)行一次,如果 N 秒內(nèi)事件再次觸發(fā),則會(huì)重新計(jì)時(shí)。
簡(jiǎn)單版:函數(shù)內(nèi)部支持使用 this 和 event 對(duì)象;
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
使用:
var node = document.getElementById('layout')
function getUserAction(e) {
console.log(this, e) // 分別打印:node 這個(gè)節(jié)點(diǎn) 和 MouseEvent
node.innerHTML = count++;
};
node.onmousemove = debounce(getUserAction, 1000)
最終版:除了支持 this 和 event 外,還支持以下功能:
支持立即執(zhí)行; 函數(shù)可能有返回值; 支持取消功能;
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已經(jīng)執(zhí)行過(guò),不再執(zhí)行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
} else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
};
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
使用:
var setUseAction = debounce(getUserAction, 10000, true);
// 使用防抖
node.onmousemove = setUseAction
// 取消防抖
setUseAction.cancel()
參考:JavaScript專(zhuān)題之跟著underscore學(xué)防抖
函數(shù)節(jié)流
觸發(fā)高頻事件,且 N 秒內(nèi)只執(zhí)行一次。
簡(jiǎn)單版:使用時(shí)間戳來(lái)實(shí)現(xiàn),立即執(zhí)行一次,然后每 N 秒執(zhí)行一次。
function throttle(func, wait) {
var context, args;
var previous = 0;
return function() {
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
最終版:支持取消節(jié)流;另外通過(guò)傳入第三個(gè)參數(shù),options.leading 來(lái)表示是否可以立即執(zhí)行一次,opitons.trailing 表示結(jié)束調(diào)用的時(shí)候是否還要執(zhí)行一次,默認(rèn)都是 true。注意設(shè)置的時(shí)候不能同時(shí)將 leading 或 trailing 設(shè)置為 false。
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}
return throttled;
}
節(jié)流的使用就不拿代碼舉例了,參考防抖的寫(xiě)就行。
參考:JavaScript專(zhuān)題之跟著 underscore 學(xué)節(jié)流
函數(shù)柯里化
什么叫函數(shù)柯里化?其實(shí)就是將使用多個(gè)參數(shù)的函數(shù)轉(zhuǎn)換成一系列使用一個(gè)參數(shù)的函數(shù)的技術(shù)。還不懂?來(lái)舉個(gè)例子。
function add(a, b, c) {
return a + b + c
}
add(1, 2, 3)
let addCurry = curry(add)
addCurry(1)(2)(3)
現(xiàn)在就是要實(shí)現(xiàn) curry 這個(gè)函數(shù),使函數(shù)從一次調(diào)用傳入多個(gè)參數(shù)變成多次調(diào)用每次傳一個(gè)參數(shù)。
function curry(fn) {
let judge = (...args) => {
if (args.length == fn.length) return fn(...args)
return (...arg) => judge(...args, ...arg)
}
return judge
}
偏函數(shù)
什么是偏函數(shù)?偏函數(shù)就是將一個(gè) n 參的函數(shù)轉(zhuǎn)換成固定 x 參的函數(shù),剩余參數(shù)(n - x)將在下次調(diào)用全部傳入。舉個(gè)例子:
function add(a, b, c) {
return a + b + c
}
let partialAdd = partial(add, 1)
partialAdd(2, 3)
發(fā)現(xiàn)沒(méi)有,其實(shí)偏函數(shù)和函數(shù)柯里化有點(diǎn)像,所以根據(jù)函數(shù)柯里化的實(shí)現(xiàn),能夠能很快寫(xiě)出偏函數(shù)的實(shí)現(xiàn):
function partial(fn, ...args) {
return (...arg) => {
return fn(...args, ...arg)
}
}
如上這個(gè)功能比較簡(jiǎn)單,現(xiàn)在我們希望偏函數(shù)能和柯里化一樣能實(shí)現(xiàn)占位功能,比如:
function clg(a, b, c) {
console.log(a, b, c)
}
let partialClg = partial(clg, '_', 2)
partialClg(1, 3) // 依次打印:1, 2, 3
_ 占的位其實(shí)就是 1 的位置。相當(dāng)于:partial(clg, 1, 2),然后 partialClg(3)。明白了原理,我們就來(lái)寫(xiě)實(shí)現(xiàn):
function partial(fn, ...args) {
return (...arg) => {
args[index] =
return fn(...args, ...arg)
}
}
JSONP
JSONP 核心原理:script 標(biāo)簽不受同源策略約束,所以可以用來(lái)進(jìn)行跨域請(qǐng)求,優(yōu)點(diǎn)是兼容性好,但是只能用于 GET 請(qǐng)求;
const jsonp = ({ url, params, callbackName }) => {
const generateUrl = () => {
let dataSrc = ''
for (let key in params) {
if (params.hasOwnProperty(key)) {
dataSrc += `${key}=${params[key]}&`
}
}
dataSrc += `callback=${callbackName}`
return `${url}?${dataSrc}`
}
return new Promise((resolve, reject) => {
const scriptEle = document.createElement('script')
scriptEle.src = generateUrl()
document.body.appendChild(scriptEle)
window[callbackName] = data => {
resolve(data)
document.removeChild(scriptEle)
}
})
}
AJAX
const getJSON = function(url) {
return new Promise((resolve, reject) => {
const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
xhr.open('GET', url, false);
xhr.setRequestHeader('Accept', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
if (xhr.status === 200 || xhr.status === 304) {
resolve(xhr.responseText);
} else {
reject(new Error(xhr.responseText));
}
}
xhr.send();
})
}
實(shí)現(xiàn)數(shù)組原型方法
forEach
Array.prototype.forEach2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this) // this 就是當(dāng)前的數(shù)組
const len = O.length >>> 0 // 后面有解釋
let k = 0
while (k < len) {
if (k in O) {
callback.call(thisArg, O[k], k, O);
}
k++;
}
}
參考:forEach#polyfill[2]
O.length >>> 0 是什么操作?就是無(wú)符號(hào)右移 0 位,那有什么意義嘛?就是為了保證轉(zhuǎn)換后的值為正整數(shù)。其實(shí)底層做了 2 層轉(zhuǎn)換,第一是非 number 轉(zhuǎn)成 number 類(lèi)型,第二是將 number 轉(zhuǎn)成 Uint32 類(lèi)型。感興趣可以閱讀 something >>> 0是什么意思?[3]。
map
基于 forEach 的實(shí)現(xiàn)能夠很容易寫(xiě)出 map 的實(shí)現(xiàn):
- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.map2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
- let k = 0
+ let k = 0, res = []
while (k < len) {
if (k in O) {
- callback.call(thisArg, O[k], k, O);
+ res[k] = callback.call(thisArg, O[k], k, O);
}
k++;
}
+ return res
}
filter
同樣,基于 forEach 的實(shí)現(xiàn)能夠很容易寫(xiě)出 filter 的實(shí)現(xiàn):
- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.filter2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
- let k = 0
+ let k = 0, res = []
while (k < len) {
if (k in O) {
- callback.call(thisArg, O[k], k, O);
+ if (callback.call(thisArg, O[k], k, O)) {
+ res.push(O[k])
+ }
}
k++;
}
+ return res
}
some
同樣,基于 forEach 的實(shí)現(xiàn)能夠很容易寫(xiě)出 some 的實(shí)現(xiàn):
- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.some2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
let k = 0
while (k < len) {
if (k in O) {
- callback.call(thisArg, O[k], k, O);
+ if (callback.call(thisArg, O[k], k, O)) {
+ return true
+ }
}
k++;
}
+ return false
}
reduce
Array.prototype.reduce2 = function(callback, initialValue) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
let k = 0, acc
if (arguments.length > 1) {
acc = initialValue
} else {
// 沒(méi)傳入初始值的時(shí)候,取數(shù)組中第一個(gè)非 empty 的值為初始值
while (k < len && !(k in O)) {
k++
}
if (k > len) {
throw new TypeError( 'Reduce of empty array with no initial value' );
}
acc = O[k++]
}
while (k < len) {
if (k in O) {
acc = callback(acc, O[k], k, O)
}
k++
}
return acc
}
實(shí)現(xiàn)函數(shù)原型方法
call
使用一個(gè)指定的 this 值和一個(gè)或多個(gè)參數(shù)來(lái)調(diào)用一個(gè)函數(shù)。
實(shí)現(xiàn)要點(diǎn):
this 可能傳入 null; 傳入不固定個(gè)數(shù)的參數(shù); 函數(shù)可能有返回值;
Function.prototype.call2 = function (context) {
var context = context || window;
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args +')');
delete context.fn
return result;
}
apply
apply 和 call 一樣,唯一的區(qū)別就是 call 是傳入不固定個(gè)數(shù)的參數(shù),而 apply 是傳入一個(gè)數(shù)組。
實(shí)現(xiàn)要點(diǎn):
this 可能傳入 null; 傳入一個(gè)數(shù)組; 函數(shù)可能有返回值;
Function.prototype.apply2 = function (context, arr) {
var context = context || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result;
}
bind
bind 方法會(huì)創(chuàng)建一個(gè)新的函數(shù),在 bind() 被調(diào)用時(shí),這個(gè)新函數(shù)的 this 被指定為 bind() 的第一個(gè)參數(shù),而其余參數(shù)將作為新函數(shù)的參數(shù),供調(diào)用時(shí)使用。
實(shí)現(xiàn)要點(diǎn):
bind() 除了 this 外,還可傳入多個(gè)參數(shù); bing 創(chuàng)建的新函數(shù)可能傳入多個(gè)參數(shù); 新函數(shù)可能被當(dāng)做構(gòu)造函數(shù)調(diào)用; 函數(shù)可能有返回值;
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
實(shí)現(xiàn) new 關(guān)鍵字
new 運(yùn)算符用來(lái)創(chuàng)建用戶(hù)自定義的對(duì)象類(lèi)型的實(shí)例或者具有構(gòu)造函數(shù)的內(nèi)置對(duì)象的實(shí)例。
實(shí)現(xiàn)要點(diǎn):
new 會(huì)產(chǎn)生一個(gè)新對(duì)象; 新對(duì)象需要能夠訪(fǎng)問(wèn)到構(gòu)造函數(shù)的屬性,所以需要重新指定它的原型; 構(gòu)造函數(shù)可能會(huì)顯示返回;
function objectFactory() {
var obj = new Object()
Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
var ret = Constructor.apply(obj, arguments);
// ret || obj 這里這么寫(xiě)考慮了構(gòu)造函數(shù)顯示返回 null 的情況
return typeof ret === 'object' ? ret || obj : obj;
};
使用:
function person(name, age) {
this.name = name
this.age = age
}
let p = objectFactory(person, '布蘭', 12)
console.log(p) // { name: '布蘭', age: 12 }
實(shí)現(xiàn) instanceof 關(guān)鍵字
instanceof 就是判斷構(gòu)造函數(shù)的 prototype 屬性是否出現(xiàn)在實(shí)例的原型鏈上。
function instanceOf(left, right) {
let proto = left.__proto__
while (true) {
if (proto === null) return false
if (proto === right.prototype) {
return true
}
proto = proto.__proto__
}
}
上面的 left.proto 這種寫(xiě)法可以換成 Object.getPrototypeOf(left)。
實(shí)現(xiàn) Object.create
Object.create()方法創(chuàng)建一個(gè)新對(duì)象,使用現(xiàn)有的對(duì)象來(lái)提供新創(chuàng)建的對(duì)象的__proto__。
Object.create2 = function(proto, propertyObject = undefined) {
if (typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object or null.')
if (propertyObject == null) {
new TypeError('Cannot convert undefined or null to object')
}
function F() {}
F.prototype = proto
const obj = new F()
if (propertyObject != undefined) {
Object.defineProperties(obj, propertyObject)
}
if (proto === null) {
// 創(chuàng)建一個(gè)沒(méi)有原型對(duì)象的對(duì)象,Object.create(null)
obj.__proto__ = null
}
return obj
}
實(shí)現(xiàn) Object.assign
Object.assign2 = function(target, ...source) {
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object')
}
let ret = Object(target)
source.forEach(function(obj) {
if (obj != null) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
ret[key] = obj[key]
}
}
}
})
return ret
}
實(shí)現(xiàn) JSON.stringify
JSON.stringify([, replacer [, space]) 方法是將一個(gè) JavaScript 值(對(duì)象或者數(shù)組)轉(zhuǎn)換為一個(gè) JSON 字符串。此處模擬實(shí)現(xiàn),不考慮可選的第二個(gè)參數(shù) replacer 和第三個(gè)參數(shù) space,如果對(duì)這兩個(gè)參數(shù)的作用還不了解,建議閱讀 MDN[4] 文檔。
基本數(shù)據(jù)類(lèi)型: undefined 轉(zhuǎn)換之后仍是 undefined(類(lèi)型也是 undefined) boolean 值轉(zhuǎn)換之后是字符串 "false"/"true" number 類(lèi)型(除了 NaN 和 Infinity)轉(zhuǎn)換之后是字符串類(lèi)型的數(shù)值 symbol 轉(zhuǎn)換之后是 undefined null 轉(zhuǎn)換之后是字符串 "null" string 轉(zhuǎn)換之后仍是string NaN 和 Infinity 轉(zhuǎn)換之后是字符串 "null" 函數(shù)類(lèi)型:轉(zhuǎn)換之后是 undefined 如果是對(duì)象類(lèi)型(非函數(shù)) 如果有 toJSON() 方法,那么序列化 toJSON() 的返回值。 如果屬性值中出現(xiàn)了 undefined、任意的函數(shù)以及 symbol 值,忽略。 所有以 symbol 為屬性鍵的屬性都會(huì)被完全忽略掉。 如果是一個(gè)數(shù)組:如果屬性值中出現(xiàn)了 undefined、任意的函數(shù)以及 symbol,轉(zhuǎn)換成字符串 "null" ; 如果是 RegExp 對(duì)象:返回 {} (類(lèi)型是 string); 如果是 Date 對(duì)象,返回 Date 的 toJSON 字符串值; 如果是普通對(duì)象; 對(duì)包含循環(huán)引用的對(duì)象(對(duì)象之間相互引用,形成無(wú)限循環(huán))執(zhí)行此方法,會(huì)拋出錯(cuò)誤。
function jsonStringify(data) {
let dataType = typeof data;
if (dataType !== 'object') {
let result = data;
//data 可能是 string/number/null/undefined/boolean
if (Number.isNaN(data) || data === Infinity) {
//NaN 和 Infinity 序列化返回 "null"
result = "null";
} else if (dataType === 'function' || dataType === 'undefined' || dataType === 'symbol') {
//function 、undefined 、symbol 序列化返回 undefined
return undefined;
} else if (dataType === 'string') {
result = '"' + data + '"';
}
//boolean 返回 String()
return String(result);
} else if (dataType === 'object') {
if (data === null) {
return "null"
} else if (data.toJSON && typeof data.toJSON === 'function') {
return jsonStringify(data.toJSON());
} else if (data instanceof Array) {
let result = [];
//如果是數(shù)組
//toJSON 方法可以存在于原型鏈中
data.forEach((item, index) => {
if (typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') {
result[index] = "null";
} else {
result[index] = jsonStringify(item);
}
});
result = "[" + result + "]";
return result.replace(/'/g, '"');
} else {
//普通對(duì)象
/**
* 循環(huán)引用拋錯(cuò)(暫未檢測(cè),循環(huán)引用時(shí),堆棧溢出)
* symbol key 忽略
* undefined、函數(shù)、symbol 為屬性值,被忽略
*/
let result = [];
Object.keys(data).forEach((item, index) => {
if (typeof item !== 'symbol') {
//key 如果是symbol對(duì)象,忽略
if (data[item] !== undefined && typeof data[item] !== 'function'
&& typeof data[item] !== 'symbol') {
//鍵值如果是 undefined、函數(shù)、symbol 為屬性值,忽略
result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
}
}
});
return ("{" + result + "}").replace(/'/g, '"');
}
}
}
參考:實(shí)現(xiàn) JSON.stringify[5]
實(shí)現(xiàn) JSON.parse
介紹 2 種方法實(shí)現(xiàn):
eval 實(shí)現(xiàn); new Function 實(shí)現(xiàn);
eval 實(shí)現(xiàn)
第一種方式最簡(jiǎn)單,也最直觀,就是直接調(diào)用 eval,代碼如下:
var json = '{"a":"1", "b":2}';
var obj = eval("(" + json + ")"); // obj 就是 json 反序列化之后得到的對(duì)象
但是直接調(diào)用 eval 會(huì)存在安全問(wèn)題,如果數(shù)據(jù)中可能不是 json 數(shù)據(jù),而是可執(zhí)行的 JavaScript 代碼,那很可能會(huì)造成 XSS 攻擊。因此,在調(diào)用 eval 之前,需要對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)。
var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
if (
rx_one.test(
json.replace(rx_two, "@")
.replace(rx_three, "]")
.replace(rx_four, "")
)
) {
var obj = eval("(" +json + ")");
}
參考:JSON.parse 三種實(shí)現(xiàn)方式[6]
new Function 實(shí)現(xiàn)
Function 與 eval 有相同的字符串參數(shù)特性。
var json = '{"name":"小姐姐", "age":20}';
var obj = (new Function('return ' + json))();
實(shí)現(xiàn) Promise
實(shí)現(xiàn) Promise 需要完全讀懂 Promise A+ 規(guī)范[7],不過(guò)從總體的實(shí)現(xiàn)上看,有如下幾個(gè)點(diǎn)需要考慮到:
then 需要支持鏈?zhǔn)秸{(diào)用,所以得返回一個(gè)新的 Promise; 處理異步問(wèn)題,所以得先用 onResolvedCallbacks 和 onRejectedCallbacks 分別把成功和失敗的回調(diào)存起來(lái); 為了讓鏈?zhǔn)秸{(diào)用正常進(jìn)行下去,需要判斷 onFulfilled 和 onRejected 的類(lèi)型; onFulfilled 和 onRejected 需要被異步調(diào)用,這里用 setTimeout 模擬異步; 處理 Promise 的 resolve;
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
let resolve = (value) = > {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach((fn) = > fn());
}
};
let reject = (reason) = > {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach((fn) = > fn());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
// 解決 onFufilled,onRejected 沒(méi)有傳值的問(wèn)題
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) = > v;
// 因?yàn)殄e(cuò)誤的值要讓后面訪(fǎng)問(wèn)到,所以這里也要拋出錯(cuò)誤,不然會(huì)在之后 then 的 resolve 中捕獲
onRejected = typeof onRejected === "function" ? onRejected : (err) = > {
throw err;
};
// 每次調(diào)用 then 都返回一個(gè)新的 promise
let promise2 = new Promise((resolve, reject) = > {
if (this.status === FULFILLED) {
//Promise/A+ 2.2.4 --- setTimeout
setTimeout(() = > {
try {
let x = onFulfilled(this.value);
// x可能是一個(gè)proimise
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === REJECTED) {
//Promise/A+ 2.2.3
setTimeout(() = > {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === PENDING) {
this.onResolvedCallbacks.push(() = > {
setTimeout(() = > {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejectedCallbacks.push(() = > {
setTimeout(() = > {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return promise2;
}
}
const resolvePromise = (promise2, x, resolve, reject) = > {
// 自己等待自己完成是錯(cuò)誤的實(shí)現(xiàn),用一個(gè)類(lèi)型錯(cuò)誤,結(jié)束掉 promise Promise/A+ 2.3.1
if (promise2 === x) {
return reject(
new TypeError("Chaining cycle detected for promise #<Promise>"));
}
// Promise/A+ 2.3.3.3.3 只能調(diào)用一次
let called;
// 后續(xù)的條件要嚴(yán)格判斷 保證代碼能和別的庫(kù)一起使用
if ((typeof x === "object" && x != null) || typeof x === "function") {
try {
// 為了判斷 resolve 過(guò)的就不用再 reject 了(比如 reject 和 resolve 同時(shí)調(diào)用的時(shí)候) Promise/A+ 2.3.3.1
let then = x.then;
if (typeof then === "function") {
// 不要寫(xiě)成 x.then,直接 then.call 就可以了 因?yàn)?nbsp;x.then 會(huì)再次取值,Object.defineProperty Promise/A+ 2.3.3.3
then.call(
x, (y) = > {
// 根據(jù) promise 的狀態(tài)決定是成功還是失敗
if (called) return;
called = true;
// 遞歸解析的過(guò)程(因?yàn)榭赡?nbsp;promise 中還有 promise) Promise/A+ 2.3.3.3.1
resolvePromise(promise2, y, resolve, reject);
}, (r) = > {
// 只要失敗就失敗 Promise/A+ 2.3.3.3.2
if (called) return;
called = true;
reject(r);
});
} else {
// 如果 x.then 是個(gè)普通值就直接返回 resolve 作為結(jié)果 Promise/A+ 2.3.3.4
resolve(x);
}
} catch (e) {
// Promise/A+ 2.3.3.2
if (called) return;
called = true;
reject(e);
}
} else {
// 如果 x 是個(gè)普通值就直接返回 resolve 作為結(jié)果 Promise/A+ 2.3.4
resolve(x);
}
};
Promise 寫(xiě)完之后可以通過(guò) promises-aplus-tests 這個(gè)包對(duì)我們寫(xiě)的代碼進(jìn)行測(cè)試,看是否符合 A+ 規(guī)范。不過(guò)測(cè)試前還得加一段代碼:
// promise.js
// 這里是上面寫(xiě)的 Promise 全部代碼
Promise.defer = Promise.deferred = function () {
let dfd = {}
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
module.exports = Promise;
全局安裝:
npm i promises-aplus-tests -g
終端下執(zhí)行驗(yàn)證命令:
promises-aplus-tests promise.js
上面寫(xiě)的代碼可以順利通過(guò)全部 872 個(gè)測(cè)試用例。
參考:
BAT前端經(jīng)典面試問(wèn)題:史上最最最詳細(xì)的手寫(xiě)Promise教程[8] 100 行代碼實(shí)現(xiàn) Promises/A+ 規(guī)范[9]
Promise.resolve
Promsie.resolve(value) 可以將任何值轉(zhuǎn)成值為 value 狀態(tài)是 fulfilled 的 Promise,但如果傳入的值本身是 Promise 則會(huì)原樣返回它。
Promise.resolve = function(value) {
// 如果是 Promsie,則直接輸出它
if(value instanceof Promise){
return value
}
return new Promise(resolve => resolve(value))
}
參考:深入理解 Promise[10]
Promise.reject
和 Promise.resolve() 類(lèi)似,Promise.reject() 會(huì)實(shí)例化一個(gè) rejected 狀態(tài)的 Promise。但與 Promise.resolve() 不同的是,如果給 Promise.reject() 傳遞一個(gè) Promise 對(duì)象,則這個(gè)對(duì)象會(huì)成為新 Promise 的值。
Promise.reject = function(reason) {
return new Promise((resolve, reject) => reject(reason))
}
Promise.all
Promise.all 的規(guī)則是這樣的:
傳入的所有 Promsie 都是 fulfilled,則返回由他們的值組成的,狀態(tài)為 fulfilled 的新 Promise; 只要有一個(gè) Promise 是 rejected,則返回 rejected 狀態(tài)的新 Promsie,且它的值是第一個(gè) rejected 的 Promise 的值; 只要有一個(gè) Promise 是 pending,則返回一個(gè) pending 狀態(tài)的新 Promise;
Promise.all = function(promiseArr) {
let index = 0, result = []
return new Promise((resolve, reject) => {
promiseArr.forEach((p, i) => {
Promise.resolve(p).then(val => {
index++
result[i] = val
if (index === promiseArr.length) {
resolve(result)
}
}, err => {
reject(err)
})
})
})
}
Promise.race
Promise.race 會(huì)返回一個(gè)由所有可迭代實(shí)例中第一個(gè) fulfilled 或 rejected 的實(shí)例包裝后的新實(shí)例。
Promise.race = function(promiseArr) {
return new Promise((resolve, reject) => {
promiseArr.forEach(p => {
Promise.resolve(p).then(val => {
resolve(val)
}, err => {
rejecte(err)
})
})
})
}
Promise.allSettled
Promise.allSettled 的規(guī)則是這樣:
所有 Promise 的狀態(tài)都變化了,那么新返回一個(gè)狀態(tài)是 fulfilled 的 Promise,且它的值是一個(gè)數(shù)組,數(shù)組的每項(xiàng)由所有 Promise 的值和狀態(tài)組成的對(duì)象; 如果有一個(gè)是 pending 的 Promise,則返回一個(gè)狀態(tài)是 pending 的新實(shí)例;
Promise.allSettled = function(promiseArr) {
let result = []
return new Promise((resolve, reject) => {
promiseArr.forEach((p, i) => {
Promise.resolve(p).then(val => {
result.push({
status: 'fulfilled',
value: val
})
if (result.length === promiseArr.length) {
resolve(result)
}
}, err => {
result.push({
status: 'rejected',
reason: err
})
if (result.length === promiseArr.length) {
resolve(result)
}
})
})
})
}
Promise.any
Promise.any 的規(guī)則是這樣:
空數(shù)組或者所有 Promise 都是 rejected,則返回狀態(tài)是 rejected 的新 Promsie,且值為 AggregateError 的錯(cuò)誤; 只要有一個(gè)是 fulfilled 狀態(tài)的,則返回第一個(gè)是 fulfilled 的新實(shí)例; 其他情況都會(huì)返回一個(gè) pending 的新實(shí)例;
Promise.any = function(promiseArr) {
let index = 0
return new Promise((resolve, reject) => {
if (promiseArr.length === 0) return
promiseArr.forEach((p, i) => {
Promise.resolve(p).then(val => {
resolve(val)
}, err => {
index++
if (index === promiseArr.length) {
reject(new AggregateError('All promises were rejected'))
}
})
})
})
}
后話(huà)
能看到這里的對(duì)代碼都是真愛(ài)了,畢竟代碼這玩意看起來(lái)是真的很枯燥,但是如果看懂了后,就會(huì)像打游戲贏了一樣開(kāi)心,而且這玩意會(huì)上癮,當(dāng)你通關(guān)了越多的關(guān)卡后,你的能力就會(huì)拔高一個(gè)層次。用標(biāo)題的話(huà)來(lái)說(shuō)就是:搞懂后,提升真的大。加油吧??,干飯人

噢不,代碼人。
覺(jué)得還行,就來(lái)個(gè)點(diǎn)贊、收藏、分享 3連吧。
參考資料
圖片懶加載: https://juejin.cn/post/6844903856489365518#heading-19
[2]forEach#polyfill: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#polyfill
[3]something >>> 0是什么意思: https://zhuanlan.zhihu.com/p/100790268
[4]stringify: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
[5]實(shí)現(xiàn) JSON.stringify: https://github.com/YvetteLau/Step-By-Step/issues/39#issuecomment-508327280
[6]JSON.parse 三種實(shí)現(xiàn)方式: https://github.com/youngwind/blog/issues/115#issue-300869613
[7]Promise A+ 規(guī)范: https://promisesaplus.com/
[8]BAT前端經(jīng)典面試問(wèn)題:史上最最最詳細(xì)的手寫(xiě)Promise教程: https://juejin.cn/post/6844903625769091079
[9]100 行代碼實(shí)現(xiàn) Promises/A+ 規(guī)范: https://mp.weixin.qq.com/s/qdJ0Xd8zTgtetFdlJL3P1g
[10]深入理解 Promise: https://bubuzou.com/2020/10/22/promise/
