死磕 36 個 JS 手寫題

作爲一個普通前端,我是真的寫不出 Promise A+ 規範,但是沒關係,我們可以站在巨人的肩膀上,要相信我們現在要走的路,前人都走過,所以可以找找現在社區已經存在的那些優秀的文章,比如工業聚大佬寫的 100 行代碼實現 Promises/A+ 規範,找到這些文章後不是收藏夾喫灰,得找個時間踏踏實實的學,一行一行的磨,直到搞懂爲止。我現在就是這麼幹的。

能收穫什麼

這篇文章總體上分爲 2 類手寫題,前半部分可以歸納爲是常見需求,後半部分則是對現有技術的實現;

閱讀的時候需要做什麼

閱讀的時候,你需要把每行代碼都看懂,知道它在幹什麼,爲什麼要這麼寫,能寫得更好嘛?比如在寫圖片懶加載的時候,一般我們都是根據當前元素的位置和視口進行判斷是否要加載這張圖片,普通程序員寫到這就差不多完成了。而大佬程序員則是會多考慮一些細節的東西,比如性能如何更優?代碼如何更精簡?比如 yeyan1996 寫的圖片懶加載就多考慮了 2 點:比如圖片全部加載完成的時候得把事件監聽給移除;比如加載完一張圖片的時候,得把當前 img 從 imgList 裏移除,起到優化內存的作用。

除了讀通代碼之外,還可以打開 Chrome 的 Script snippet 去寫測試用例跑跑代碼,做到更好的理解以及使用。

在看了幾篇以及寫了很多測試用例的前提下,嘗試自己手寫實現,看看自己到底掌握了多少。條條大路通羅馬,你還能有別的方式實現嘛?或者你能寫得比別人更好嘛?

好了,還楞着幹啥,開始幹活。

數據類型判斷

typeof 可以正確識別:Undefined、Boolean、Number、String、Symbol、Function 等類型的數據,但是對於其他的都會認爲是 object,比如 Null、Date 等,所以通過 typeof 來判斷數據類型會不準確。但是可以使用 Object.prototype.toString 實現。

 1function typeOf(obj) {
 2    let res = Object.prototype.toString.call(obj).split(' ')[1]
 3    res = res.substring(0, res.length - 1).toLowerCase()
 4    return res
 5}
 6typeOf([])        // 'array'
 7typeOf({})        // 'object'
 8typeOf(new Date)  // 'date'
 9
10

繼承

原型鏈繼承

 1function Animal() {
 2    this.colors = ['black', 'white']
 3}
 4Animal.prototype.getColor = function() {
 5    return this.colors
 6}
 7function Dog() {}
 8Dog.prototype =  new Animal()
 9
10let dog1 = new Dog()
11dog1.colors.push('brown')
12let dog2 = new Dog()
13console.log(dog2.colors)  // ['black', 'white', 'brown']
14
15

原型鏈繼承存在的問題:

借用構造函數實現繼承

 1function Animal(name) {
 2    this.name = name
 3    this.getName = function() {
 4        return this.name
 5    }
 6}
 7function Dog(name) {
 8    Animal.call(this, name)
 9}
10Dog.prototype =  new Animal()
11
12

借用構造函數實現繼承解決了原型鏈繼承的 2 個問題:引用類型共享問題以及傳參問題。但是由於方法必須定義在構造函數中,所以會導致每次創建子類實例都會創建一遍方法。

組合繼承

組合繼承結合了原型鏈和盜用構造函數,將兩者的優點集中了起來。基本的思路是使用原型鏈繼承原型上的屬性和方法,而通過盜用構造函數繼承實例屬性。這樣既可以把方法定義在原型上以實現重用,又可以讓每個實例都有自己的屬性。

 1function Animal(name) {
 2    this.name = name
 3    this.colors = ['black', 'white']
 4}
 5Animal.prototype.getName = function() {
 6    return this.name
 7}
 8function Dog(name, age) {
 9    Animal.call(this, name)
10    this.age = age
11}
12Dog.prototype =  new Animal()
13Dog.prototype.constructor = Dog
14
15let dog1 = new Dog('奶昔', 2)
16dog1.colors.push('brown')
17let dog2 = new Dog('哈赤', 1)
18console.log(dog2) 
19// { name: "哈赤", colors: ["black", "white"], age: 1 }
20
21

寄生式組合繼承

組合繼承已經相對完善了,但還是存在問題,它的問題就是調用了 2 次父類構造函數,第一次是在 new Animal(),第二次是在 Animal.call() 這裏。

所以解決方案就是不直接調用父類構造函數給子類原型賦值,而是通過創建空函數 F 獲取父類原型的副本。

寄生式組合繼承寫法上和組合繼承基本類似,區別是如下這裏:

 1- Dog.prototype =  new Animal()
 2- Dog.prototype.constructor = Dog
 3
 4+ function F() {}
 5+ F.prototype = Animal.prototype
 6+ let f = new F()
 7+ f.constructor = Dog
 8+ Dog.prototype = f
 9
10

稍微封裝下上面添加的代碼後:

 1function object(o) {
 2    function F() {}
 3    F.prototype = o
 4    return new F()
 5}
 6function inheritPrototype(child, parent) {
 7    let prototype = object(parent.prototype)
 8    prototype.constructor = child
 9    child.prototype = prototype
10}
11inheritPrototype(Dog, Animal)
12
13

如果你嫌棄上面的代碼太多了,還可以基於組合繼承的代碼改成最簡單的寄生式組合繼承:

1- Dog.prototype =  new Animal()
2- Dog.prototype.constructor = Dog
3
4+ Dog.prototype =  Object.create(Animal.prototype)
5+ Dog.prototype.constructor = Dog
6
7

class 實現繼承

 1class Animal {
 2    constructor(name) {
 3        this.name = name
 4    } 
 5    getName() {
 6        return this.name
 7    }
 8}
 9class Dog extends Animal {
10    constructor(name, age) {
11        super(name)
12        this.age = age
13    }
14}
15
16

數組去重

ES5 實現:

1function unique(arr) {
2    var res = arr.filter(function(item, index, array) {
3        return array.indexOf(item) === index
4    })
5    return res
6}
7
8

ES6 實現:

1var unique = arr => [...new Set(arr)]
2
3

數組扁平化

數組扁平化就是將 [1, [2, [3]]] 這種多層的數組拍平成一層 [1, 2, 3]。使用 Array.prototype.flat 可以直接將多層數組拍平成一層:

1[1, [2, [3]]].flat(2)  // [1, 2, 3]
2
3

現在就是要實現 flat 這種效果。

ES5 實現:遞歸。

 1function flatten(arr) {
 2    var result = [];
 3    for (var i = 0, len = arr.length; i < len; i++) {
 4        if (Array.isArray(arr[i])) {
 5            result = result.concat(flatten(arr[i]))
 6        } else {
 7            result.push(arr[i])
 8        }
 9    }
10    return result;
11}
12
13

ES6 實現:

1function flatten(arr) {
2    while (arr.some(item => Array.isArray(item))) {
3        arr = [].concat(...arr);
4    }
5    return arr;
6}
7
8

深淺拷貝

淺拷貝:只考慮對象類型。

 1function shallowCopy(obj) {
 2    if (typeof obj !== 'object') return
 3    
 4    let newObj = obj instanceof Array ? [] : {}
 5    for (let key in obj) {
 6        if (obj.hasOwnProperty(key)) {
 7            newObj[key] = obj[key]
 8        }
 9    }
10    return newObj
11}
12
13

簡單版深拷貝:只考慮普通對象屬性,不考慮內置對象和函數。

 1function deepClone(obj) {
 2    if (typeof obj !== 'object') return;
 3    var newObj = obj instanceof Array ? [] : {};
 4    for (var key in obj) {
 5        if (obj.hasOwnProperty(key)) {
 6            newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
 7        }
 8    }
 9    return newObj;
10}
11
12

複雜版深克隆:基於簡單版的基礎上,還考慮了內置對象比如 Date、RegExp 等對象和函數以及解決了循環引用的問題。

 1const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;
 2
 3function deepClone(target, map = new WeakMap()) {
 4    if (map.get(target)) {
 5        return target;
 6    }
 7    // 獲取當前值的構造函數:獲取它的類型
 8    let constructor = target.constructor;
 9    // 檢測當前對象target是否與正則、日期格式對象匹配
10    if (/^(RegExp|Date)$/i.test(constructor.name)) {
11        // 創建一個新的特殊對象(正則類/日期類)的實例
12        return new constructor(target);  
13    }
14    if (isObject(target)) {
15        map.set(target, true);  // 爲循環引用的對象做標記
16        const cloneTarget = Array.isArray(target) ? [] : {};
17        for (let prop in target) {
18            if (target.hasOwnProperty(prop)) {
19                cloneTarget[prop] = deepClone(target[prop], map);
20            }
21        }
22        return cloneTarget;
23    } else {
24        return target;
25    }
26}
27
28

事件總線(發佈訂閱模式)

 1class EventEmitter {
 2    constructor() {
 3        this.cache = {}
 4    }
 5    on(name, fn) {
 6        if (this.cache[name]) {
 7            this.cache[name].push(fn)
 8        } else {
 9            this.cache[name] = [fn]
10        }
11    }
12    off(name, fn) {
13        let tasks = this.cache[name]
14        if (tasks) {
15            const index = tasks.findIndex(f => f === fn || f.callback === fn)
16            if (index >= 0) {
17                tasks.splice(index, 1)
18            }
19        }
20    }
21    emit(name, once = false, ...args) {
22        if (this.cache[name]) {
23            // 創建副本,如果回調函數內繼續註冊相同事件,會造成死循環
24            let tasks = this.cache[name].slice()
25            for (let fn of tasks) {
26                fn(...args)
27            }
28            if (once) {
29                delete this.cache[name]
30            }
31        }
32    }
33}
34
35// 測試
36let eventBus = new EventEmitter()
37let fn1 = function(name, age) {
38    console.log(`${name} ${age}`)
39}
40let fn2 = function(name, age) {
41    console.log(`hello, ${name} ${age}`)
42}
43eventBus.on('aaa', fn1)
44eventBus.on('aaa', fn2)
45eventBus.emit('aaa', false, '布蘭', 12)
46// '布蘭 12'
47// 'hello, 布蘭 12'
48
49

解析 URL 參數爲對象

 1function parseParam(url) {
 2    const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 將 ? 後面的字符串取出來
 3    const paramsArr = paramsStr.split('&'); // 將字符串以 & 分割後存到數組中
 4    let paramsObj = {};
 5    // 將 params 存到對象中
 6    paramsArr.forEach(param => {
 7        if (/=/.test(param)) { // 處理有 value 的參數
 8            let [key, val] = param.split('='); // 分割 key 和 value
 9            val = decodeURIComponent(val); // 解碼
10            val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判斷是否轉爲數字
11    
12            if (paramsObj.hasOwnProperty(key)) { // 如果對象有 key,則添加一個值
13                paramsObj[key] = [].concat(paramsObj[key], val);
14            } else { // 如果對象沒有這個 key,創建 key 並設置值
15                paramsObj[key] = val;
16            }
17        } else { // 處理沒有 value 的參數
18            paramsObj[param] = true;
19        }
20    })
21    
22    return paramsObj;
23}
24
25

字符串模板

 1function render(template, data) {
 2    const reg = /\{\{(\w+)\}\}/; // 模板字符串正則
 3    if (reg.test(template)) { // 判斷模板裏是否有模板字符串
 4        const name = reg.exec(template)[1]; // 查找當前模板裏第一個模板字符串的字段
 5        template = template.replace(reg, data[name]); // 將第一個模板字符串渲染
 6        return render(template, data); // 遞歸的渲染並返回渲染後的結構
 7    }
 8    return template; // 如果模板沒有模板字符串直接返回
 9}
10
11

測試:

1let template = '我是{{name}},年齡{{age}},性別{{sex}}';
2let person = {
3    name: '布蘭',
4    age: 12
5}
6render(template, person); // 我是布蘭,年齡12,性別undefined
7
8

圖片懶加載

與普通的圖片懶加載不同,如下這個多做了 2 個精心處理:

 1let imgList = [...document.querySelectorAll('img')]
 2let length = imgList.length
 3
 4const imgLazyLoad = function() {
 5    let count = 0
 6    return function() {
 7        let deleteIndexList = []
 8        imgList.forEach((img, index) => {
 9            let rect = img.getBoundingClientRect()
10            if (rect.top < window.innerHeight) {
11                img.src = img.dataset.src
12                deleteIndexList.push(index)
13                count++
14                if (count === length) {
15                    document.removeEventListener('scroll', imgLazyLoad)
16                }
17            }
18        })
19        imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
20    }
21}
22
23// 這裏最好加上防抖處理
24document.addEventListener('scroll', imgLazyLoad)
25
26

參考:圖片懶加載 [1]

函數防抖

觸發高頻事件 N 秒後只會執行一次,如果 N 秒內事件再次觸發,則會重新計時。

簡單版:函數內部支持使用 this 和 event 對象;

 1function debounce(func, wait) {
 2    var timeout;
 3    return function () {
 4        var context = this;
 5        var args = arguments;
 6        clearTimeout(timeout)
 7        timeout = setTimeout(function(){
 8            func.apply(context, args)
 9        }, wait);
10    }
11}
12
13

使用:

1var node = document.getElementById('layout')
2function getUserAction(e) {
3    console.log(this, e)  // 分別打印:node 這個節點 和 MouseEvent
4    node.innerHTML = count++;
5};
6node.onmousemove = debounce(getUserAction, 1000)
7
8

最終版:除了支持 this 和 event 外,還支持以下功能:

 1function debounce(func, wait, immediate) {
 2    var timeout, result;
 3    
 4    var debounced = function () {
 5        var context = this;
 6        var args = arguments;
 7        
 8        if (timeout) clearTimeout(timeout);
 9        if (immediate) {
10            // 如果已經執行過,不再執行
11            var callNow = !timeout;
12            timeout = setTimeout(function(){
13                timeout = null;
14            }, wait)
15            if (callNow) result = func.apply(context, args)
16        } else {
17            timeout = setTimeout(function(){
18                func.apply(context, args)
19            }, wait);
20        }
21        return result;
22    };
23
24    debounced.cancel = function() {
25        clearTimeout(timeout);
26        timeout = null;
27    };
28
29    return debounced;
30}
31
32

使用:

1var setUseAction = debounce(getUserAction, 10000, true);
2// 使用防抖
3node.onmousemove = setUseAction
4
5// 取消防抖
6setUseAction.cancel()
7
8

參考:JavaScript 專題之跟着 underscore 學防抖

函數節流

觸發高頻事件,且 N 秒內只執行一次。

簡單版:使用時間戳來實現,立即執行一次,然後每 N 秒執行一次。

 1function throttle(func, wait) {
 2    var context, args;
 3    var previous = 0;
 4
 5    return function() {
 6        var now = +new Date();
 7        context = this;
 8        args = arguments;
 9        if (now - previous > wait) {
10            func.apply(context, args);
11            previous = now;
12        }
13    }
14}
15
16

最終版:支持取消節流;另外通過傳入第三個參數,options.leading 來表示是否可以立即執行一次,opitons.trailing 表示結束調用的時候是否還要執行一次,默認都是 true。注意設置的時候不能同時將 leading 或 trailing 設置爲 false。

 1function throttle(func, wait, options) {
 2    var timeout, context, args, result;
 3    var previous = 0;
 4    if (!options) options = {};
 5
 6    var later = function() {
 7        previous = options.leading === false ? 0 : new Date().getTime();
 8        timeout = null;
 9        func.apply(context, args);
10        if (!timeout) context = args = null;
11    };
12
13    var throttled = function() {
14        var now = new Date().getTime();
15        if (!previous && options.leading === false) previous = now;
16        var remaining = wait - (now - previous);
17        context = this;
18        args = arguments;
19        if (remaining <= 0 || remaining > wait) {
20            if (timeout) {
21                clearTimeout(timeout);
22                timeout = null;
23            }
24            previous = now;
25            func.apply(context, args);
26            if (!timeout) context = args = null;
27        } else if (!timeout && options.trailing !== false) {
28            timeout = setTimeout(later, remaining);
29        }
30    };
31    
32    throttled.cancel = function() {
33        clearTimeout(timeout);
34        previous = 0;
35        timeout = null;
36    }
37    return throttled;
38}
39
40

節流的使用就不拿代碼舉例了,參考防抖的寫就行。

參考:JavaScript 專題之跟着 underscore 學節流

函數柯里化

什麼叫函數柯里化?其實就是將使用多個參數的函數轉換成一系列使用一個參數的函數的技術。還不懂?來舉個例子。

1function add(a, b, c) {
2    return a + b + c
3}
4add(1, 2, 3)
5let addCurry = curry(add)
6addCurry(1)(2)(3)
7
8

現在就是要實現 curry 這個函數,使函數從一次調用傳入多個參數變成多次調用每次傳一個參數。

1function curry(fn) {
2    let judge = (...args) => {
3        if (args.length == fn.length) return fn(...args)
4        return (...arg) => judge(...args, ...arg)
5    }
6    return judge
7}
8
9

偏函數

什麼是偏函數?偏函數就是將一個 n 參的函數轉換成固定 x 參的函數,剩餘參數(n - x)將在下次調用全部傳入。舉個例子:

1function add(a, b, c) {
2    return a + b + c
3}
4let partialAdd = partial(add, 1)
5partialAdd(2, 3)
6
7

發現沒有,其實偏函數和函數柯里化有點像,所以根據函數柯里化的實現,能夠能很快寫出偏函數的實現:

1function partial(fn, ...args) {
2    return (...arg) => {
3        return fn(...args, ...arg)
4    }
5}
6
7

如上這個功能比較簡單,現在我們希望偏函數能和柯里化一樣能實現佔位功能,比如:

1function clg(a, b, c) {
2    console.log(a, b, c)
3}
4let partialClg = partial(clg, '_', 2)
5partialClg(1, 3)  // 依次打印:1, 2, 3
6
7

_ 佔的位其實就是 1 的位置。相當於:partial(clg, 1, 2),然後 partialClg(3)。明白了原理,我們就來寫實現:

1function partial(fn, ...args) {
2    return (...arg) => {
3        args[index] = 
4        return fn(...args, ...arg)
5    }
6}
7
8

JSONP

JSONP 核心原理:script 標籤不受同源策略約束,所以可以用來進行跨域請求,優點是兼容性好,但是隻能用於 GET 請求;

 1const jsonp = ({ url, params, callbackName }) => {
 2    const generateUrl = () => {
 3        let dataSrc = ''
 4        for (let key in params) {
 5            if (params.hasOwnProperty(key)) {
 6                dataSrc += `${key}=${params[key]}&`
 7            }
 8        }
 9        dataSrc += `callback=${callbackName}`
10        return `${url}?${dataSrc}`
11    }
12    return new Promise((resolve, reject) => {
13        const scriptEle = document.createElement('script')
14        scriptEle.src = generateUrl()
15        document.body.appendChild(scriptEle)
16        window[callbackName] = data => {
17            resolve(data)
18            document.removeChild(scriptEle)
19        }
20    })
21}
22
23

AJAX

 1const getJSON = function(url) {
 2    return new Promise((resolve, reject) => {
 3        const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
 4        xhr.open('GET', url, false);
 5        xhr.setRequestHeader('Accept', 'application/json');
 6        xhr.onreadystatechange = function() {
 7            if (xhr.readyState !== 4) return;
 8            if (xhr.status === 200 || xhr.status === 304) {
 9                resolve(xhr.responseText);
10            } else {
11                reject(new Error(xhr.responseText));
12            }
13        }
14        xhr.send();
15    })
16}
17
18

實現數組原型方法

forEach

 1Array.prototype.forEach2 = function(callback, thisArg) {
 2    if (this == null) {
 3        throw new TypeError('this is null or not defined')
 4    }
 5    if (typeof callback !== "function") {
 6        throw new TypeError(callback + ' is not a function')
 7    }
 8    const O = Object(this)  // this 就是當前的數組
 9    const len = O.length >>> 0  // 後面有解釋
10    let k = 0
11    while (k < len) {
12        if (k in O) {
13            callback.call(thisArg, O[k], k, O);
14        }
15        k++;
16    }
17}
18
19

參考:forEach#polyfill[2]

O.length >>> 0 是什麼操作?就是無符號右移 0 位,那有什麼意義嘛?就是爲了保證轉換後的值爲正整數。其實底層做了 2 層轉換,第一是非 number 轉成 number 類型,第二是將 number 轉成 Uint32 類型。感興趣可以閱讀 something >>> 0 是什麼意思?[3]。

map

基於 forEach 的實現能夠很容易寫出 map 的實現:

 1- Array.prototype.forEach2 = function(callback, thisArg) {
 2+ Array.prototype.map2 = function(callback, thisArg) {
 3    if (this == null) {
 4        throw new TypeError('this is null or not defined')
 5    }
 6    if (typeof callback !== "function") {
 7        throw new TypeError(callback + ' is not a function')
 8    }
 9    const O = Object(this)
10    const len = O.length >>> 0
11-   let k = 0
12+   let k = 0, res = []
13    while (k < len) {
14        if (k in O) {
15-           callback.call(thisArg, O[k], k, O);
16+           res[k] = callback.call(thisArg, O[k], k, O);
17        }
18        k++;
19    }
20+   return res
21}
22
23

filter

同樣,基於 forEach 的實現能夠很容易寫出 filter 的實現:

 1- Array.prototype.forEach2 = function(callback, thisArg) {
 2+ Array.prototype.filter2 = function(callback, thisArg) {
 3    if (this == null) {
 4        throw new TypeError('this is null or not defined')
 5    }
 6    if (typeof callback !== "function") {
 7        throw new TypeError(callback + ' is not a function')
 8    }
 9    const O = Object(this)
10    const len = O.length >>> 0
11-   let k = 0
12+   let k = 0, res = []
13    while (k < len) {
14        if (k in O) {
15-           callback.call(thisArg, O[k], k, O);
16+           if (callback.call(thisArg, O[k], k, O)) {
17+               res.push(O[k])                
18+           }
19        }
20        k++;
21    }
22+   return res
23}
24
25

some

同樣,基於 forEach 的實現能夠很容易寫出 some 的實現:

 1- Array.prototype.forEach2 = function(callback, thisArg) {
 2+ Array.prototype.some2 = function(callback, thisArg) {
 3    if (this == null) {
 4        throw new TypeError('this is null or not defined')
 5    }
 6    if (typeof callback !== "function") {
 7        throw new TypeError(callback + ' is not a function')
 8    }
 9    const O = Object(this)
10    const len = O.length >>> 0
11    let k = 0
12    while (k < len) {
13        if (k in O) {
14-           callback.call(thisArg, O[k], k, O);
15+           if (callback.call(thisArg, O[k], k, O)) {
16+               return true
17+           }
18        }
19        k++;
20    }
21+   return false
22}
23
24

reduce

 1Array.prototype.reduce2 = function(callback, initialValue) {
 2    if (this == null) {
 3        throw new TypeError('this is null or not defined')
 4    }
 5    if (typeof callback !== "function") {
 6        throw new TypeError(callback + ' is not a function')
 7    }
 8    const O = Object(this)
 9    const len = O.length >>> 0
10    let k = 0, acc
11    
12    if (arguments.length > 1) {
13        acc = initialValue
14    } else {
15        // 沒傳入初始值的時候,取數組中第一個非 empty 的值爲初始值
16        while (k < len && !(k in O)) {
17            k++
18        }
19        if (k > len) {
20            throw new TypeError( 'Reduce of empty array with no initial value' );
21        }
22        acc = O[k++]
23    }
24    while (k < len) {
25        if (k in O) {
26            acc = callback(acc, O[k], k, O)
27        }
28        k++
29    }
30    return acc
31}
32
33

實現函數原型方法

call

使用一個指定的 this 值和一個或多個參數來調用一個函數。

實現要點:

 1Function.prototype.call2 = function (context) {
 2    var context = context || window;
 3    context.fn = this;
 4
 5    var args = [];
 6    for(var i = 1, len = arguments.length; i < len; i++) {
 7        args.push('arguments[' + i + ']');
 8    }
 9
10    var result = eval('context.fn(' + args +')');
11
12    delete context.fn
13    return result;
14}
15
16

apply

apply 和 call 一樣,唯一的區別就是 call 是傳入不固定個數的參數,而 apply 是傳入一個數組。

實現要點:

 1Function.prototype.apply2 = function (context, arr) {
 2    var context = context || window;
 3    context.fn = this;
 4
 5    var result;
 6    if (!arr) {
 7        result = context.fn();
 8    } else {
 9        var args = [];
10        for (var i = 0, len = arr.length; i < len; i++) {
11            args.push('arr[' + i + ']');
12        }
13        result = eval('context.fn(' + args + ')')
14    }
15
16    delete context.fn
17    return result;
18}
19
20

bind

bind 方法會創建一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定爲 bind() 的第一個參數,而其餘參數將作爲新函數的參數,供調用時使用。

實現要點:

 1Function.prototype.bind2 = function (context) {
 2    var self = this;
 3    var args = Array.prototype.slice.call(arguments, 1);
 4
 5    var fNOP = function () {};
 6
 7    var fBound = function () {
 8        var bindArgs = Array.prototype.slice.call(arguments);
 9        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
10    }
11
12    fNOP.prototype = this.prototype;
13    fBound.prototype = new fNOP();
14    return fBound;
15}
16
17

實現 new 關鍵字

new 運算符用來創建用戶自定義的對象類型的實例或者具有構造函數的內置對象的實例。

實現要點:

 1function objectFactory() {
 2    var obj = new Object()
 3    Constructor = [].shift.call(arguments);
 4    obj.__proto__ = Constructor.prototype;
 5    var ret = Constructor.apply(obj, arguments);
 6    
 7    // ret || obj 這裏這麼寫考慮了構造函數顯示返回 null 的情況
 8    return typeof ret === 'object' ? ret || obj : obj;
 9};
10
11

使用:

1function person(name, age) {
2    this.name = name
3    this.age = age
4}
5let p = objectFactory(person, '布蘭', 12)
6console.log(p)  // { name: '布蘭', age: 12 }
7
8

實現 instanceof 關鍵字

instanceof 就是判斷構造函數的 prototype 屬性是否出現在實例的原型鏈上。

 1function instanceOf(left, right) {
 2    let proto = left.__proto__
 3    while (true) {
 4        if (proto === null) return false
 5        if (proto === right.prototype) {
 6            return true
 7        }
 8        proto = proto.__proto__
 9    }
10}
11
12

上面的 left.proto 這種寫法可以換成 Object.getPrototypeOf(left)。

實現 Object.create

Object.create() 方法創建一個新對象,使用現有的對象來提供新創建的對象的__proto__。

 1Object.create2 = function(proto, propertyObject = undefined) {
 2    if (typeof proto !== 'object' && typeof proto !== 'function') {
 3        throw new TypeError('Object prototype may only be an Object or null.')
 4    if (propertyObject == null) {
 5        new TypeError('Cannot convert undefined or null to object')
 6    }
 7    function F() {}
 8    F.prototype = proto
 9    const obj = new F()
10    if (propertyObject != undefined) {
11        Object.defineProperties(obj, propertyObject)
12    }
13    if (proto === null) {
14        // 創建一個沒有原型對象的對象,Object.create(null)
15        obj.__proto__ = null
16    }
17    return obj
18}
19
20

實現 Object.assign

 1Object.assign2 = function(target, ...source) {
 2    if (target == null) {
 3        throw new TypeError('Cannot convert undefined or null to object')
 4    }
 5    let ret = Object(target) 
 6    source.forEach(function(obj) {
 7        if (obj != null) {
 8            for (let key in obj) {
 9                if (obj.hasOwnProperty(key)) {
10                    ret[key] = obj[key]
11                }
12            }
13        }
14    })
15    return ret
16}
17
18

實現 JSON.stringify

JSON.stringify([, replacer [, space]) 方法是將一個 JavaScript 值 (對象或者數組) 轉換爲一個 JSON 字符串。此處模擬實現,不考慮可選的第二個參數 replacer 和第三個參數 space,如果對這兩個參數的作用還不瞭解,建議閱讀 MDN[4] 文檔。

  1. 基本數據類型:
  1. 函數類型:轉換之後是 undefined

  2. 如果是對象類型 (非函數)

  1. 對包含循環引用的對象(對象之間相互引用,形成無限循環)執行此方法,會拋出錯誤。
 1function jsonStringify(data) {
 2    let dataType = typeof data;
 3    
 4    if (dataType !== 'object') {
 5        let result = data;
 6        //data 可能是 string/number/null/undefined/boolean
 7        if (Number.isNaN(data) || data === Infinity) {
 8            //NaN 和 Infinity 序列化返回 "null"
 9            result = "null";
10        } else if (dataType === 'function' || dataType === 'undefined' || dataType === 'symbol') {
11            //function 、undefined 、symbol 序列化返回 undefined
12            return undefined;
13        } else if (dataType === 'string') {
14            result = '"' + data + '"';
15        }
16        //boolean 返回 String()
17        return String(result);
18    } else if (dataType === 'object') {
19        if (data === null) {
20            return "null"
21        } else if (data.toJSON && typeof data.toJSON === 'function') {
22            return jsonStringify(data.toJSON());
23        } else if (data instanceof Array) {
24            let result = [];
25            //如果是數組
26            //toJSON 方法可以存在於原型鏈中
27            data.forEach((item, index) => {
28                if (typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') {
29                    result[index] = "null";
30                } else {
31                    result[index] = jsonStringify(item);
32                }
33            });
34            result = "[" + result + "]";
35            return result.replace(/'/g, '"');
36            
37        } else {
38            //普通對象
39            /**
40             * 循環引用拋錯(暫未檢測,循環引用時,堆棧溢出)
41             * symbol key 忽略
42             * undefined、函數、symbol 爲屬性值,被忽略
43             */
44            let result = [];
45            Object.keys(data).forEach((item, index) => {
46                if (typeof item !== 'symbol') {
47                    //key 如果是symbol對象,忽略
48                    if (data[item] !== undefined && typeof data[item] !== 'function'
49                        && typeof data[item] !== 'symbol') {
50                        //鍵值如果是 undefined、函數、symbol 爲屬性值,忽略
51                        result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
52                    }
53                }
54            });
55            return ("{" + result + "}").replace(/'/g, '"');
56        }
57    }
58}
59
60

參考:實現 JSON.stringify[5]

實現 JSON.parse

介紹 2 種方法實現:

eval 實現

第一種方式最簡單,也最直觀,就是直接調用 eval,代碼如下:

1var json = '{"a":"1", "b":2}';
2var obj = eval("(" + json + ")");  // obj 就是 json 反序列化之後得到的對象
3
4

但是直接調用 eval 會存在安全問題,如果數據中可能不是 json 數據,而是可執行的 JavaScript 代碼,那很可能會造成 XSS 攻擊。因此,在調用 eval 之前,需要對數據進行校驗。

 1var rx_one = /^[\],:{}\s]*$/;
 2var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
 3var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
 4var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
 5
 6if (
 7    rx_one.test(
 8        json.replace(rx_two, "@")
 9            .replace(rx_three, "]")
10            .replace(rx_four, "")
11    )
12) {
13    var obj = eval("(" +json + ")");
14}
15
16

參考:JSON.parse 三種實現方式 [6]

new Function 實現

Function 與 eval 有相同的字符串參數特性。

1var json = '{"name":"小姐姐", "age":20}';
2var obj = (new Function('return ' + json))();
3
4

實現 Promise

實現 Promise 需要完全讀懂 Promise A+ 規範 [7],不過從總體的實現上看,有如下幾個點需要考慮到:

  1const PENDING = 'pending';
  2const FULFILLED = 'fulfilled';
  3const REJECTED = 'rejected';
  4
  5class Promise {
  6    constructor(executor) {
  7        this.status = PENDING;
  8        this.value = undefined;
  9        this.reason = undefined;
 10        this.onResolvedCallbacks = [];
 11        this.onRejectedCallbacks = [];
 12        
 13        let resolve = (value) = > {
 14            if (this.status === PENDING) {
 15                this.status = FULFILLED;
 16                this.value = value;
 17                this.onResolvedCallbacks.forEach((fn) = > fn());
 18            }
 19        };
 20        
 21        let reject = (reason) = > {
 22            if (this.status === PENDING) {
 23                this.status = REJECTED;
 24                this.reason = reason;
 25                this.onRejectedCallbacks.forEach((fn) = > fn());
 26            }
 27        };
 28        
 29        try {
 30            executor(resolve, reject);
 31        } catch (error) {
 32            reject(error);
 33        }
 34    }
 35    
 36    then(onFulfilled, onRejected) {
 37        // 解決 onFufilled,onRejected 沒有傳值的問題
 38        onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) = > v;
 39        // 因爲錯誤的值要讓後面訪問到,所以這裏也要拋出錯誤,不然會在之後 then 的 resolve 中捕獲
 40        onRejected = typeof onRejected === "function" ? onRejected : (err) = > {
 41            throw err;
 42        };
 43        // 每次調用 then 都返回一個新的 promise
 44        let promise2 = new Promise((resolve, reject) = > {
 45            if (this.status === FULFILLED) {
 46                //Promise/A+ 2.2.4 --- setTimeout
 47                setTimeout(() = > {
 48                    try {
 49                        let x = onFulfilled(this.value);
 50                        // x可能是一個proimise
 51                        resolvePromise(promise2, x, resolve, reject);
 52                    } catch (e) {
 53                        reject(e);
 54                    }
 55                }, 0);
 56            }
 57        
 58            if (this.status === REJECTED) {
 59                //Promise/A+ 2.2.3
 60                setTimeout(() = > {
 61                    try {
 62                        let x = onRejected(this.reason);
 63                        resolvePromise(promise2, x, resolve, reject);
 64                    } catch (e) {
 65                        reject(e);
 66                    }
 67                }, 0);
 68            }
 69            
 70            if (this.status === PENDING) {
 71                this.onResolvedCallbacks.push(() = > {
 72                    setTimeout(() = > {
 73                        try {
 74                            let x = onFulfilled(this.value);
 75                            resolvePromise(promise2, x, resolve, reject);
 76                        } catch (e) {
 77                            reject(e);
 78                        }
 79                    }, 0);
 80                });
 81            
 82                this.onRejectedCallbacks.push(() = > {
 83                    setTimeout(() = > {
 84                        try {
 85                            let x = onRejected(this.reason);
 86                            resolvePromise(promise2, x, resolve, reject);
 87                        } catch (e) {
 88                            reject(e);
 89                        }
 90                    }, 0);
 91                });
 92            }
 93        });
 94        
 95        return promise2;
 96    }
 97}
 98const resolvePromise = (promise2, x, resolve, reject) = > {
 99    // 自己等待自己完成是錯誤的實現,用一個類型錯誤,結束掉 promise  Promise/A+ 2.3.1
100    if (promise2 === x) {
101        return reject(
102            new TypeError("Chaining cycle detected for promise #<Promise>"));
103    }
104    // Promise/A+ 2.3.3.3.3 只能調用一次
105    let called;
106    // 後續的條件要嚴格判斷 保證代碼能和別的庫一起使用
107    if ((typeof x === "object" && x != null) || typeof x === "function") {
108        try {
109            // 爲了判斷 resolve 過的就不用再 reject 了(比如 reject 和 resolve 同時調用的時候)  Promise/A+ 2.3.3.1
110            let then = x.then;
111            if (typeof then === "function") {
112            // 不要寫成 x.then,直接 then.call 就可以了 因爲 x.then 會再次取值,Object.defineProperty  Promise/A+ 2.3.3.3
113                then.call(
114                    x, (y) = > {
115                        // 根據 promise 的狀態決定是成功還是失敗
116                        if (called) return;
117                        called = true;
118                        // 遞歸解析的過程(因爲可能 promise 中還有 promise) Promise/A+ 2.3.3.3.1
119                        resolvePromise(promise2, y, resolve, reject);
120                    }, (r) = > {
121                        // 只要失敗就失敗 Promise/A+ 2.3.3.3.2
122                        if (called) return;
123                        called = true;
124                        reject(r);
125                    });
126            } else {
127                // 如果 x.then 是個普通值就直接返回 resolve 作爲結果  Promise/A+ 2.3.3.4
128                resolve(x);
129            }
130        } catch (e) {
131            // Promise/A+ 2.3.3.2
132            if (called) return;
133            called = true;
134            reject(e);
135        }
136    } else {
137        // 如果 x 是個普通值就直接返回 resolve 作爲結果  Promise/A+ 2.3.4
138        resolve(x);
139    }
140};
141
142

Promise 寫完之後可以通過 promises-aplus-tests 這個包對我們寫的代碼進行測試,看是否符合 A+ 規範。不過測試前還得加一段代碼:

 1// promise.js
 2// 這裏是上面寫的 Promise 全部代碼
 3Promise.defer = Promise.deferred = function () {
 4    let dfd = {}
 5    dfd.promise = new Promise((resolve,reject)=>{
 6        dfd.resolve = resolve;
 7        dfd.reject = reject;
 8    });
 9    return dfd;
10}
11module.exports = Promise;
12
13
14

全局安裝:

1npm i promises-aplus-tests -g
2
3

終端下執行驗證命令:

1promises-aplus-tests promise.js
2
3

上面寫的代碼可以順利通過全部 872 個測試用例。

參考:

Promise.resolve

Promsie.resolve(value) 可以將任何值轉成值爲 value 狀態是 fulfilled 的 Promise,但如果傳入的值本身是 Promise 則會原樣返回它。

1Promise.resolve = function(value) {
2    // 如果是 Promsie,則直接輸出它
3    if(value instanceof Promise){
4        return value
5    }
6    return new Promise(resolve => resolve(value))
7}
8
9

參考:深入理解 Promise[10]

Promise.reject

和 Promise.resolve() 類似,Promise.reject() 會實例化一個 rejected 狀態的 Promise。但與 Promise.resolve() 不同的是,如果給 Promise.reject() 傳遞一個 Promise 對象,則這個對象會成爲新 Promise 的值。

1Promise.reject = function(reason) {
2    return new Promise((resolve, reject) => reject(reason))
3}
4
5

Promise.all

Promise.all 的規則是這樣的:

 1Promise.all = function(promiseArr) {
 2    let index = 0, result = []
 3    return new Promise((resolve, reject) => {
 4        promiseArr.forEach((p, i) => {
 5            Promise.resolve(p).then(val => {
 6                index++
 7                result[i] = val
 8                if (index === promiseArr.length) {
 9                    resolve(result)
10                }
11            }, err => {
12                reject(err)
13            })
14        })
15    })
16}
17
18

Promise.race

Promise.race 會返回一個由所有可迭代實例中第一個 fulfilled 或 rejected 的實例包裝後的新實例。

 1Promise.race = function(promiseArr) {
 2    return new Promise((resolve, reject) => {
 3        promiseArr.forEach(p => {
 4            Promise.resolve(p).then(val => {
 5                resolve(val)
 6            }, err => {
 7                rejecte(err)
 8            })
 9        })
10    })
11}
12
13

Promise.allSettled

Promise.allSettled 的規則是這樣:

 1Promise.allSettled = function(promiseArr) {
 2    let result = []
 3        
 4    return new Promise((resolve, reject) => {
 5        promiseArr.forEach((p, i) => {
 6            Promise.resolve(p).then(val => {
 7                result.push({
 8                    status: 'fulfilled',
 9                    value: val
10                })
11                if (result.length === promiseArr.length) {
12                    resolve(result) 
13                }
14            }, err => {
15                result.push({
16                    status: 'rejected',
17                    reason: err
18                })
19                if (result.length === promiseArr.length) {
20                    resolve(result) 
21                }
22            })
23        })  
24    })   
25}
26
27

Promise.any

Promise.any 的規則是這樣:

 1Promise.any = function(promiseArr) {
 2    let index = 0
 3    return new Promise((resolve, reject) => {
 4        if (promiseArr.length === 0) return 
 5        promiseArr.forEach((p, i) => {
 6            Promise.resolve(p).then(val => {
 7                resolve(val)
 8                
 9            }, err => {
10                index++
11                if (index === promiseArr.length) {
12                  reject(new AggregateError('All promises were rejected'))
13                }
14            })
15        })
16    })
17}
18
19

後話

能看到這裏的對代碼都是真愛了,畢竟代碼這玩意看起來是真的很枯燥,但是如果看懂了後,就會像打遊戲贏了一樣開心,而且這玩意會上癮,當你通關了越多的關卡後,你的能力就會拔高一個層次。用標題的話來說就是:搞懂後,提升真的大。加油吧💪,乾飯人

噢不,代碼人。

參考資料

[1]

圖片懶加載: 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]

實現 JSON.stringify: https://github.com/YvetteLau/Step-By-Step/issues/39#issuecomment-508327280

[6]

JSON.parse 三種實現方式: https://github.com/youngwind/blog/issues/115#issue-300869613

[7]

Promise A+ 規範: https://promisesaplus.com/

[8]

BAT 前端經典面試問題:史上最最最詳細的手寫 Promise 教程: https://juejin.cn/post/6844903625769091079

[9]

100 行代碼實現 Promises/A+ 規範: https://mp.weixin.qq.com/s/qdJ0Xd8zTgtetFdlJL3P1g

[10]

深入理解 Promise: https://bubuzou.com/2020/10/22/promise/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/YXEiK8NNx-Bo81371rVCaQ