死磕 36 個 JS 手寫題(搞懂後,提高真的大)

爲何要寫這類文章前端

做爲一個程序員,代碼能力毋庸置疑是很是很是重要的,就像如今爲何大廠面試基本都問什麼 API 怎麼實現可見其重要性。我想說的是竟然手寫這麼重要,那咱們就必須掌握它,因此文章標題用了死磕,一點也不過度,也但願不被認爲是標題黨。node

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

能收穫什麼程序員

這篇文章整體上分爲 2 類手寫題,前半部分能夠概括爲是常見需求,後半部分則是對現有技術的實現;github

  • 對經常使用的需求進行手寫實現,好比數據類型判斷函數、深拷貝等能夠直接用於日後的項目中,提升了項目開發效率;
  • 對現有關鍵字和 API 的實現,可能須要用到別的知識或 API,好比在寫 forEach 的時候用到了無符號位右移的操做,平時都不怎麼可以接觸到這玩意,如今遇到了就能夠順手把它掌握了。因此手寫這些實現可以潛移默化的擴展並鞏固本身的 JS 基礎;
  • 經過寫各類測試用例,你會知道各類 API 的邊界狀況,好比 Promise.all, 你得考慮到傳入參數的各類狀況,從而加深了對它們的理解及使用;

閱讀的時候須要作什麼面試

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

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

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

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

數據類型判斷

typeof 能夠正確識別:Undefined、Boolean、Number、String、Symbol、Function 等類型的數據,可是對於其餘的都會認爲是 object,好比 Null、Date 等,因此經過 typeof 來判斷數據類型會不許確。可是可使用 Object.prototype.toString 實現。

function typeOf(obj) {
- let res = Object.prototype.toString.call(obj).split(' ')[1]
- res = res.substring(0, res.length - 1).toLowerCase()
- return res
// 評論區裏提到的更好的寫法
+ return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()
}
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']
複製代碼

原型鏈繼承存在的問題:

  • 問題1:原型中包含的引用類型屬性將被全部實例共享;
  • 問題2:子類在實例化的時候不能給父類構造函數傳參;

借用構造函數實現繼承

function Animal(name) {
    this.name = name
    this.getName = function() {
        return this.name
    }
}
function Dog(name) {
    Animal.call(this, name)
}
Dog.prototype =  new Animal()
複製代碼

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

組合繼承

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

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 }
複製代碼

寄生式組合繼承

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

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

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

- 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)
複製代碼

若是你嫌棄上面的代碼太多了,還能夠基於組合繼承的代碼改爲最簡單的寄生式組合繼承:

- Dog.prototype = new Animal()
- Dog.prototype.constructor = Dog

+ Dog.prototype = Object.create(Animal.prototype)
+ Dog.prototype.constructor = Dog
複製代碼

class 實現繼承

class Animal {
    constructor(name) {
        this.name = name
    } 
    getName() {
        return this.name
    }
}
class Dog extends Animal {
    constructor(name, age) {
        super(name)
        this.age = age
    }
}
複製代碼

數組去重

ES5 實現:

function unique(arr) {
    var res = arr.filter(function(item, index, array) {
        return array.indexOf(item) === index
    })
    return res
}
複製代碼

ES6 實現:

var unique = arr => [...new Set(arr)]
複製代碼

數組扁平化

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

[1, [2, [3]]].flat(2)  // [1, 2, 3]
複製代碼

如今就是要實現 flat 這種效果。

ES5 實現:遞歸。

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 實現:

function flatten(arr) {
    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}
複製代碼

深淺拷貝

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

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
}
複製代碼

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

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;
}
複製代碼

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

const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;

function deepClone(target, map = new WeakMap()) {
    if (map.get(target)) {
        return target;
    }
    // 獲取當前值的構造函數:獲取它的類型
    let constructor = target.constructor;
    // 檢測當前對象target是否與正則、日期格式對象匹配
    if (/^(RegExp|Date)$/i.test(constructor.name)) {
        // 建立一個新的特殊對象(正則類/日期類)的實例
        return new constructor(target);  
    }
    if (isObject(target)) {
        map.set(target, true);  // 爲循環引用的對象作標記
        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;
    }
}
複製代碼

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

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]) {
            // 建立副本,若是回調函數內繼續註冊相同事件,會形成死循環
            let tasks = this.cache[name].slice()
            for (let fn of tasks) {
                fn(...args)
            }
            if (once) {
                delete this.cache[name]
            }
        }
    }
}

// 測試
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 參數爲對象

function parseParam(url) {
    const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 將 ? 後面的字符串取出來
    const paramsArr = paramsStr.split('&'); // 將字符串以 & 分割後存到數組中
    let paramsObj = {};
    // 將 params 存到對象中
    paramsArr.forEach(param => {
        if (/=/.test(param)) { // 處理有 value 的參數
            let [key, val] = param.split('='); // 分割 key 和 value
            val = decodeURIComponent(val); // 解碼
            val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判斷是否轉爲數字
    
            if (paramsObj.hasOwnProperty(key)) { // 若是對象有 key,則添加一個值
                paramsObj[key] = [].concat(paramsObj[key], val);
            } else { // 若是對象沒有這個 key,建立 key 並設置值
                paramsObj[key] = val;
            }
        } else { // 處理沒有 value 的參數
            paramsObj[param] = true;
        }
    })
    
    return paramsObj;
}
複製代碼

字符串模板

function render(template, data) {
    const reg = /\{\{(\w+)\}\}/; // 模板字符串正則
    if (reg.test(template)) { // 判斷模板裏是否有模板字符串
        const name = reg.exec(template)[1]; // 查找當前模板裏第一個模板字符串的字段
        template = template.replace(reg, data[name]); // 將第一個模板字符串渲染
        return render(template, data); // 遞歸的渲染並返回渲染後的結構
    }
    return template; // 若是模板沒有模板字符串直接返回
}
複製代碼

測試:

let template = '我是{{name}},年齡{{age}},性別{{sex}}';
let person = {
    name: '布蘭',
    age: 12
}
render(template, person); // 我是布蘭,年齡12,性別undefined
複製代碼

圖片懶加載

與普通的圖片懶加載不一樣,以下這個多作了 2 個精心處理:

  • 圖片所有加載完成後移除事件監聽;
  • 加載完的圖片,從 imgList 移除;
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length

// 修正錯誤,須要加上自執行
- const imgLazyLoad = function() {
+ 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)
複製代碼

參考:圖片懶加載

函數防抖

觸發高頻事件 N 秒後只會執行一次,若是 N 秒內事件再次觸發,則會從新計時。

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

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 這個節點 和 MouseEvent
    node.innerHTML = count++;
};
node.onmousemove = debounce(getUserAction, 1000)
複製代碼

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

  • 支持當即執行;
  • 函數可能有返回值;
  • 支持取消功能;
function debounce(func, wait, immediate) {
    var timeout, result;
    
    var debounced = function () {
        var context = this;
        var args = arguments;
        
        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 若是已經執行過,再也不執行
            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專題之跟着underscore學防抖

函數節流

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

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

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;
        }
    }
}
複製代碼

最終版:支持取消節流;另外經過傳入第三個參數,options.leading 來表示是否能夠當即執行一次,opitons.trailing 表示結束調用的時候是否還要執行一次,默認都是 true。 注意設置的時候不能同時將 leading 或 trailing 設置爲 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;
}
複製代碼

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

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

函數柯里化

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

function add(a, b, c) {
    return a + b + c
}
add(1, 2, 3)
let addCurry = curry(add)
addCurry(1)(2)(3)
複製代碼

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

function curry(fn) {
    let judge = (...args) => {
        if (args.length == fn.length) return fn(...args)
        return (...arg) => judge(...args, ...arg)
    }
    return judge
}
複製代碼

偏函數

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

function add(a, b, c) {
    return a + b + c
}
let partialAdd = partial(add, 1)
partialAdd(2, 3)
複製代碼

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

function partial(fn, ...args) {
    return (...arg) => {
        return fn(...args, ...arg)
    }
}
複製代碼

如上這個功能比較簡單,如今咱們但願偏函數能和柯里化同樣能實現佔位功能,好比:

function clg(a, b, c) {
    console.log(a, b, c)
}
let partialClg = partial(clg, '_', 2)
partialClg(1, 3)  // 依次打印:1, 2, 3
複製代碼

_ 佔的位其實就是 1 的位置。至關於:partial(clg, 1, 2),而後 partialClg(3)。明白了原理,咱們就來寫實現:

function partial(fn, ...args) {
    return (...arg) => {
        args[index] = 
        return fn(...args, ...arg)
    }
}
複製代碼

JSONP

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

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();
    })
}
複製代碼

實現數組原型方法

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 就是當前的數組
    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

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

map

基於 forEach 的實現可以很容易寫出 map 的實現:

- 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 的實現可以很容易寫出 filter 的實現:

- 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 的實現可以很容易寫出 some 的實現:

- 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 {
        // 沒傳入初始值的時候,取數組中第一個非 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
}
複製代碼

實現函數原型方法

call

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

實現要點:

  • this 可能傳入 null;
  • 傳入不固定個數的參數;
  • 函數可能有返回值;
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 同樣,惟一的區別就是 call 是傳入不固定個數的參數,而 apply 是傳入一個數組。

實現要點:

  • this 可能傳入 null;
  • 傳入一個數組;
  • 函數可能有返回值;
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 方法會建立一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定爲 bind() 的第一個參數,而其他參數將做爲新函數的參數,供調用時使用。

實現要點:

  • bind() 除了 this 外,還可傳入多個參數;
  • bing 建立的新函數可能傳入多個參數;
  • 新函數可能被當作構造函數調用;
  • 函數可能有返回值;
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;
}
複製代碼

實現 new 關鍵字

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

實現要點:

  • new 會產生一個新對象;
  • 新對象須要可以訪問到構造函數的屬性,因此須要從新指定它的原型;
  • 構造函數可能會顯示返回;
function objectFactory() {
    var obj = new Object()
    Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype;
    var ret = Constructor.apply(obj, arguments);
    
    // ret || obj 這裏這麼寫考慮了構造函數顯示返回 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 }
複製代碼

實現 instanceof 關鍵字

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

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 這種寫法能夠換成 Object.getPrototypeOf(left)。

實現 Object.create

Object.create()方法建立一個新對象,使用現有的對象來提供新建立的對象的__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) {
        // 建立一個沒有原型對象的對象,Object.create(null)
        obj.__proto__ = null
    }
    return obj
}
複製代碼

實現 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
}
複製代碼

實現 JSON.stringify

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

  1. 基本數據類型:
    • undefined 轉換以後還是 undefined(類型也是 undefined)
    • boolean 值轉換以後是字符串 "false"/"true"
    • number 類型(除了 NaN 和 Infinity)轉換以後是字符串類型的數值
    • symbol 轉換以後是 undefined
    • null 轉換以後是字符串 "null"
    • string 轉換以後還是string
    • NaN 和 Infinity 轉換以後是字符串 "null"
  2. 函數類型:轉換以後是 undefined
  3. 若是是對象類型(非函數)
    • 若是是一個數組:若是屬性值中出現了 undefined、任意的函數以及 symbol,轉換成字符串 "null" ;
    • 若是是 RegExp 對象:返回 {} (類型是 string);
    • 若是是 Date 對象,返回 Date 的 toJSON 字符串值;
    • 若是是普通對象;
      • 若是有 toJSON() 方法,那麼序列化 toJSON() 的返回值。
      • 若是屬性值中出現了 undefined、任意的函數以及 symbol 值,忽略。
      • 全部以 symbol 爲屬性鍵的屬性都會被徹底忽略掉。
  4. 對包含循環引用的對象(對象之間相互引用,造成無限循環)執行此方法,會拋出錯誤。
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 = [];
            //若是是數組
            //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 {
            //普通對象
            /** * 循環引用拋錯(暫未檢測,循環引用時,堆棧溢出) * symbol key 忽略 * undefined、函數、symbol 爲屬性值,被忽略 */
            let result = [];
            Object.keys(data).forEach((item, index) => {
                if (typeof item !== 'symbol') {
                    //key 若是是symbol對象,忽略
                    if (data[item] !== undefined && typeof data[item] !== 'function'
                        && typeof data[item] !== 'symbol') {
                        //鍵值若是是 undefined、函數、symbol 爲屬性值,忽略
                        result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
                    }
                }
            });
            return ("{" + result + "}").replace(/'/g, '"');
        }
    }
}
複製代碼

參考:實現 JSON.stringify

實現 JSON.parse

介紹 2 種方法實現:

  • eval 實現;
  • new Function 實現;

eval 實現

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

var json = '{"a":"1", "b":2}';
var obj = eval("(" + json + ")");  // obj 就是 json 反序列化以後獲得的對象
複製代碼

可是直接調用 eval 會存在安全問題,若是數據中可能不是 json 數據,而是可執行的 JavaScript 代碼,那極可能會形成 XSS 攻擊。所以,在調用 eval 以前,須要對數據進行校驗。

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 三種實現方式

new Function 實現

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

var json = '{"name":"小姐姐", "age":20}';
var obj = (new Function('return ' + json))();
複製代碼

實現 Promise

實現 Promise 須要徹底讀懂 Promise A+ 規範,不過從整體的實現上看,有以下幾個點須要考慮到:

  • then 須要支持鏈式調用,因此得返回一個新的 Promise;
  • 處理異步問題,因此得先用 onResolvedCallbacks 和 onRejectedCallbacks 分別把成功和失敗的回調存起來;
  • 爲了讓鏈式調用正常進行下去,須要判斷 onFulfilled 和 onRejected 的類型;
  • onFulfilled 和 onRejected 須要被異步調用,這裏用 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 沒有傳值的問題
        onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) = > v;
        // 由於錯誤的值要讓後面訪問到,因此這裏也要拋出錯誤,否則會在以後 then 的 resolve 中捕獲
        onRejected = typeof onRejected === "function" ? onRejected : (err) = > {
            throw err;
        };
        // 每次調用 then 都返回一個新的 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多是一個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) = > {
    // 本身等待本身完成是錯誤的實現,用一個類型錯誤,結束掉 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 只能調用一次
    let called;
    // 後續的條件要嚴格判斷 保證代碼能和別的庫一塊兒使用
    if ((typeof x === "object" && x != null) || typeof x === "function") {
        try {
            // 爲了判斷 resolve 過的就不用再 reject 了(好比 reject 和 resolve 同時調用的時候) Promise/A+ 2.3.3.1
            let then = x.then;
            if (typeof then === "function") {
            // 不要寫成 x.then,直接 then.call 就能夠了 由於 x.then 會再次取值,Object.defineProperty Promise/A+ 2.3.3.3
                then.call(
                    x, (y) = > {
                        // 根據 promise 的狀態決定是成功仍是失敗
                        if (called) return;
                        called = true;
                        // 遞歸解析的過程(由於可能 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 是個普通值就直接返回 resolve 做爲結果 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 是個普通值就直接返回 resolve 做爲結果 Promise/A+ 2.3.4
        resolve(x);
    }
};
複製代碼

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

// promise.js
// 這裏是上面寫的 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
複製代碼

終端下執行驗證命令:

promises-aplus-tests promise.js
複製代碼

上面寫的代碼能夠順利經過所有 872 個測試用例。

參考:

Promise.resolve

Promsie.resolve(value) 能夠將任何值轉成值爲 value 狀態是 fulfilled 的 Promise,但若是傳入的值自己是 Promise 則會原樣返回它。

Promise.resolve = function(value) {
    // 若是是 Promsie,則直接輸出它
    if(value instanceof Promise){
        return value
    }
    return new Promise(resolve => resolve(value))
}
複製代碼

參考:深刻理解 Promise

Promise.reject

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

Promise.reject = function(reason) {
    return new Promise((resolve, reject) => reject(reason))
}
複製代碼

Promise.all

Promise.all 的規則是這樣的:

  • 傳入的全部 Promsie 都是 fulfilled,則返回由他們的值組成的,狀態爲 fulfilled 的新 Promise;
  • 只要有一個 Promise 是 rejected,則返回 rejected 狀態的新 Promsie,且它的值是第一個 rejected 的 Promise 的值;
  • 只要有一個 Promise 是 pending,則返回一個 pending 狀態的新 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 會返回一個由全部可迭代實例中第一個 fulfilled 或 rejected 的實例包裝後的新實例。

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 的規則是這樣:

  • 全部 Promise 的狀態都變化了,那麼新返回一個狀態是 fulfilled 的 Promise,且它的值是一個數組,數組的每項由全部 Promise 的值和狀態組成的對象;
  • 若是有一個是 pending 的 Promise,則返回一個狀態是 pending 的新實例;
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 的規則是這樣:

  • 空數組或者全部 Promise 都是 rejected,則返回狀態是 rejected 的新 Promsie,且值爲 AggregateError 的錯誤;
  • 只要有一個是 fulfilled 狀態的,則返回第一個是 fulfilled 的新實例;
  • 其餘狀況都會返回一個 pending 的新實例;
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'))
                }
            })
        })
    })
}
複製代碼

後話

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

image.png

噢不,代碼人。

相關文章
相關標籤/搜索