「中高階前端面試」JavaScript手寫程式碼無敵祕籍

前端勸退師發表於2019-03-28

手寫路徑導航

1. 實現一個new操作符

來源:「你不知道的javascript」 英文版

new操作符做了這些事:

  • 它建立了一個全新的物件。
  • 它會被執行[[Prototype]](也就是__proto__)連結。
  • 它使this指向新建立的物件。。
  • 通過new建立的每個物件將最終被[[Prototype]]連結到這個函式的prototype物件上。
  • 如果函式沒有返回物件型別Object(包含Functoin, Array, Date, RegExg, Error),那麼new表示式中的函式呼叫將返回該物件引用。
function New(func) {
    var res = {};
    if (func.prototype !== null) {
        res.__proto__ = func.prototype;
    }
    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
    if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
        return ret;
    }
    return res;
}
var obj = New(A, 1, 2);
// equals to
var obj = new A(1, 2);
複製程式碼

2. 實現一個JSON.stringify

JSON.stringify(value[, replacer [, space]])

  • Boolean | Number| String 型別會自動轉換成對應的原始值。
  • undefined、任意函式以及symbol,會被忽略(出現在非陣列物件的屬性值中時),或者被轉換成 null(出現在陣列中時)。
  • 不可列舉的屬性會被忽略
  • 如果一個物件的屬性值通過某種間接的方式指回該物件本身,即迴圈引用,屬性也會被忽略。
function jsonStringify(obj) {
    let type = typeof obj;
    if (type !== "object" || type === null) {
        if (/string|undefined|function/.test(type)) {
            obj = '"' + obj + '"';
        }
        return String(obj);
    } else {
        let json = []
        arr = (obj && obj.constructor === Array);
        for (let k in obj) {
            let v = obj[k];
            let type = typeof v;
            if (/string|undefined|function/.test(type)) {
                v = '"' + v + '"';
            } else if (type === "object") {
                v = jsonStringify(v);
            }
            json.push((arr ? "" : '"' + k + '":') + String(v));
        }
        return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}")
    }
}
jsonStringify({x : 5}) // "{"x":5}"
jsonStringify([1, "false", false]) // "[1,"false",false]"
jsonStringify({b: undefined}) // "{"b":"undefined"}"
複製程式碼

3. 實現一個JSON.parse

JSON.parse(text[, reviver])

用來解析JSON字串,構造由字串描述的JavaScript值或物件。提供可選的reviver函式用以在返回之前對所得到的物件執行變換(操作)。

3.1 第一種:直接呼叫 eval

function jsonParse(opt) {
    return eval('(' + opt + ')');
}
jsonParse(jsonStringify({x : 5}))
// Object { x: 5}
jsonParse(jsonStringify([1, "false", false]))
// [1, "false", falsr]
jsonParse(jsonStringify({b: undefined}))
// Object { b: "undefined"}
複製程式碼

避免在不必要的情況下使用 eval,eval() 是一個危險的函式, 他執行的程式碼擁有著執行者的權利。如果你用 eval()執行的字串程式碼被惡意方(不懷好意的人)操控修改,您最終可能會在您的網頁/擴充套件程式的許可權下,在使用者計算機上執行惡意程式碼。

它會執行JS程式碼,有XSS漏洞。

如果你只想記這個方法,就得對引數json做校驗。

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 + ")");
}
複製程式碼

3.2 第二種:Function

來源 神奇的eval()與new Function()

核心:Functioneval有相同的字串引數特性。

var func = new Function(arg1, arg2, ..., functionBody);

在轉換JSON的實際應用中,只需要這麼做。

var jsonStr = '{ "age": 20, "name": "jack" }'
var json = (new Function('return ' + jsonStr))();
複製程式碼

evalFunction 都有著動態編譯js程式碼的作用,但是在實際的程式設計中並不推薦使用。

這裡是面向面試程式設計,寫這兩種就夠了。至於第三,第四種,涉及到繁瑣的遞迴和狀態機相關原理,具體可以看:

《JSON.parse 三種實現方式》

4. 實現一個callapply

call語法:

fun.call(thisArg, arg1, arg2, ...),呼叫一個函式, 其具有一個指定的this值和分別地提供的引數(引數的列表)。

apply語法:

func.apply(thisArg, [argsArray]),呼叫一個函式,以及作為一個陣列(或類似陣列物件)提供的引數。

4.1 Function.call按套路實現

call核心:

  • 將函式設為物件的屬性
  • 執行&刪除這個函式
  • 指定this到函式並傳入給定引數執行函式
  • 如果不傳入引數,預設指向為 window

為啥說是套路實現呢?因為真實面試中,面試官很喜歡讓你逐步地往深考慮,這時候你可以反套路他,先寫個簡單版的:

4.1.1 簡單版

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
}
foo.bar() // 1
複製程式碼

4.1.2 完善版

當面試官有進一步的發問,或者此時你可以假裝思考一下。然後寫出以下版本:

Function.prototype.call2 = function(content = window) {
    content.fn = this;
    let args = [...arguments].slice(1);
    let result = content.fn(...args);
    delect content.fn;
    return result;
}
var foo = {
    value: 1
}
function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}
bar.call2(foo, 'black', '18') // black 18 1
複製程式碼

4.2 Function.apply的模擬實現

apply()的實現和call()類似,只是引數形式不同。直接貼程式碼吧:

Function.prototype.apply2 = function(context = window) {
    context.fn = this
    let result;
    // 判斷是否有第二個引數
    if(arguments[1]) {
        result = context.fn(...arguments[1])
    } else {
        result = context.fn()
    }
    delete context.fn()
    return result
}
複製程式碼

5. 實現一個Function.bind()

bind()方法:

會建立一個新函式。當這個新函式被呼叫時,bind() 的第一個引數將作為它執行時的 this,之後的一序列引數將會在傳遞的實參前傳入作為它的引數。(來自於 MDN )

此外,bind實現需要考慮例項化後對原型鏈的影響。

Function.prototype.bind2 = function(content) {
    if(typeof this != "function") {
        throw Error("not a function")
    }
    // 若沒問引數型別則從這開始寫
    let fn = this;
    let args = [...arguments].slice(1);
    
    let resFn = function() {
        return fn.apply(this.instance == resFn ? this : content,args.concat(...arguments) )
    }
    function tmp() {}
    tmp.prototype = this.prototype;
    resFn.prototype = new tmp();
    
    return resFn;
}
複製程式碼

6.實現一個繼承

寄生組合式繼承

一般只建議寫這種,因為其它方式的繼承會在一次例項中呼叫兩次父類的建構函式或有其它缺點。

核心實現是:用一個 F 空的建構函式去取代執行了 Parent 這個建構函式。

function Parent(name) {
    this.name = name;
}
Parent.prototype.sayName = function() {
    console.log('parent name:', this.name);
}
function Child(name, parentName) {
    Parent.call(this, parentName);  
    this.name = name;    
}
function create(proto) {
    function F(){}
    F.prototype = proto;
    return new F();
}
Child.prototype = create(Parent.prototype);
Child.prototype.sayName = function() {
    console.log('child name:', this.name);
}
Child.prototype.constructor = Child;

var parent = new Parent('father');
parent.sayName();    // parent name: father

var child = new Child('son', 'father');
複製程式碼

7.實現一個JS函式柯里化

「中高階前端面試」JavaScript手寫程式碼無敵祕籍
什麼是柯里化?

在電腦科學中,柯里化(Currying)是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數且返回結果的新函式的技術。

函式柯里化的主要作用和特點就是引數複用、提前返回和延遲執行。

7.1 通用版

function multi() {
    var args = Array.prototype.slice.call(arguments);
	var fn = function() {
		var newArgs = args.concat(Array.prototype.slice.call(arguments));
        return multi.apply(this, newArgs);
    }
    fn.toString = function() {
        return args.reduce(function(a, b) {
            return a * b;
        })
    }
    return fn;
}
function multiFn(a, b, c) {
    return a * b * c;
}
var multi = curry(multiFn);

multi(2)(3)(4);
multi(2,3,4);
multi(2)(3,4);
multi(2,3)(4);
複製程式碼

7.2 ES6騷寫法

const curry = (fn, arr = []) => (...args) => (
  arg => arg.length === fn.length
    ? fn(...arg)
    : curry(fn, arg)
)([...arr, ...args])
複製程式碼

8.手寫一個Promise(中高階必考)

我們來過一遍Promise/A+規範:

  • 三種狀態pending| fulfilled(resolved) | rejected
  • 當處於pending狀態的時候,可以轉移到fulfilled(resolved)或者rejected狀態
  • 當處於fulfilled(resolved)狀態或者rejected狀態的時候,就不可變。
  1. 必須有一個then非同步執行方法,then接受兩個引數且必須返回一個promise:
// onFulfilled 用來接收promise成功的值
// onRejected 用來接收promise失敗的原因
promise1=promise.then(onFulfilled, onRejected);
複製程式碼

8.1 Promise的流程圖分析

「中高階前端面試」JavaScript手寫程式碼無敵祕籍
來回顧下Promise用法:

var promise = new Promise((resolve,reject) => {
    if (操作成功) {
        resolve(value)
    } else {
        reject(error)
    }
})
promise.then(function (value) {
    // success
},function (value) {
    // failure
})
複製程式碼

8.2 面試夠用版

來源:實現一個完美符合Promise/A+規範的Promise

function myPromise(constructor){
    let self=this;
    self.status="pending" //定義狀態改變前的初始狀態
    self.value=undefined;//定義狀態為resolved的時候的狀態
    self.reason=undefined;//定義狀態為rejected的時候的狀態
    function resolve(value){
        //兩個==="pending",保證了狀態的改變是不可逆的
       if(self.status==="pending"){
          self.value=value;
          self.status="resolved";
       }
    }
    function reject(reason){
        //兩個==="pending",保證了狀態的改變是不可逆的
       if(self.status==="pending"){
          self.reason=reason;
          self.status="rejected";
       }
    }
    //捕獲構造異常
    try{
       constructor(resolve,reject);
    }catch(e){
       reject(e);
    }
}
複製程式碼

同時,需要在myPromise的原型上定義鏈式呼叫的then方法:

myPromise.prototype.then=function(onFullfilled,onRejected){
   let self=this;
   switch(self.status){
      case "resolved":
        onFullfilled(self.value);
        break;
      case "rejected":
        onRejected(self.reason);
        break;
      default:       
   }
}
複製程式碼

測試一下:

var p=new myPromise(function(resolve,reject){resolve(1)});
p.then(function(x){console.log(x)})
//輸出1
複製程式碼

8.3 大廠專供版

直接貼出來吧,這個版本還算好理解

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

function Promise(excutor) {
    let that = this; // 快取當前promise例項物件
    that.status = PENDING; // 初始狀態
    that.value = undefined; // fulfilled狀態時 返回的資訊
    that.reason = undefined; // rejected狀態時 拒絕的原因
    that.onFulfilledCallbacks = []; // 儲存fulfilled狀態對應的onFulfilled函式
    that.onRejectedCallbacks = []; // 儲存rejected狀態對應的onRejected函式

    function resolve(value) { // value成功態時接收的終值
        if(value instanceof Promise) {
            return value.then(resolve, reject);
        }
        // 實踐中要確保 onFulfilled 和 onRejected 方法非同步執行,且應該在 then 方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。
        setTimeout(() => {
            // 呼叫resolve 回撥對應onFulfilled函式
            if (that.status === PENDING) {
                // 只能由pending狀態 => fulfilled狀態 (避免呼叫多次resolve reject)
                that.status = FULFILLED;
                that.value = value;
                that.onFulfilledCallbacks.forEach(cb => cb(that.value));
            }
        });
    }
    function reject(reason) { // reason失敗態時接收的拒因
        setTimeout(() => {
            // 呼叫reject 回撥對應onRejected函式
            if (that.status === PENDING) {
                // 只能由pending狀態 => rejected狀態 (避免呼叫多次resolve reject)
                that.status = REJECTED;
                that.reason = reason;
                that.onRejectedCallbacks.forEach(cb => cb(that.reason));
            }
        });
    }

    // 捕獲在excutor執行器中丟擲的異常
    // new Promise((resolve, reject) => {
    //     throw new Error('error in excutor')
    // })
    try {
        excutor(resolve, reject);
    } catch (e) {
        reject(e);
    }
}

Promise.prototype.then = function(onFulfilled, onRejected) {
    const that = this;
    let newPromise;
    // 處理引數預設值 保證引數後續能夠繼續執行
    onFulfilled =
        typeof onFulfilled === "function" ? onFulfilled : value => value;
    onRejected =
        typeof onRejected === "function" ? onRejected : reason => {
            throw reason;
        };
    if (that.status === FULFILLED) { // 成功態
        return newPromise = new Promise((resolve, reject) => {
            setTimeout(() => {
                try{
                    let x = onFulfilled(that.value);
                    resolvePromise(newPromise, x, resolve, reject); // 新的promise resolve 上一個onFulfilled的返回值
                } catch(e) {
                    reject(e); // 捕獲前面onFulfilled中丟擲的異常 then(onFulfilled, onRejected);
                }
            });
        })
    }

    if (that.status === REJECTED) { // 失敗態
        return newPromise = new Promise((resolve, reject) => {
            setTimeout(() => {
                try {
                    let x = onRejected(that.reason);
                    resolvePromise(newPromise, x, resolve, reject);
                } catch(e) {
                    reject(e);
                }
            });
        });
    }

    if (that.status === PENDING) { // 等待態
        // 當非同步呼叫resolve/rejected時 將onFulfilled/onRejected收集暫存到集合中
        return newPromise = new Promise((resolve, reject) => {
            that.onFulfilledCallbacks.push((value) => {
                try {
                    let x = onFulfilled(value);
                    resolvePromise(newPromise, x, resolve, reject);
                } catch(e) {
                    reject(e);
                }
            });
            that.onRejectedCallbacks.push((reason) => {
                try {
                    let x = onRejected(reason);
                    resolvePromise(newPromise, x, resolve, reject);
                } catch(e) {
                    reject(e);
                }
            });
        });
    }
};
複製程式碼

「中高階前端面試」JavaScript手寫程式碼無敵祕籍
emmm,我還是乖乖地寫回進階版吧。

9. 手寫防抖(Debouncing)和節流(Throttling)

scroll 事件本身會觸發頁面的重新渲染,同時 scroll 事件的 handler 又會被高頻度的觸發, 因此事件的 handler 內部不應該有複雜操作,例如 DOM 操作就不應該放在事件處理中。 針對此類高頻度觸發事件問題(例如頁面 scroll ,螢幕 resize,監聽使用者輸入等),有兩種常用的解決方法,防抖和節流。

9.1 防抖(Debouncing)實現

典型例子:限制 滑鼠連擊 觸發。

一個比較好的解釋是:

當一次事件發生後,事件處理器要等一定閾值的時間,如果這段時間過去後 再也沒有 事件發生,就處理最後一次發生的事件。假設還差 0.01 秒就到達指定時間,這時又來了一個事件,那麼之前的等待作廢,需要重新再等待指定時間。

「中高階前端面試」JavaScript手寫程式碼無敵祕籍

// 防抖動函式
function debounce(fn,wait=50,immediate) {
    let timer;
    return function() {
        if(immediate) {
            fn.apply(this,arguments)
        }
        if(timer) clearTimeout(timer)
        timer = setTimeout(()=> {
            fn.apply(this,arguments)
        },wait)
    }
}
複製程式碼

9.2 節流(Throttling)實現

可以理解為事件在一個管道中傳輸,加上這個節流閥以後,事件的流速就會減慢。實際上這個函式的作用就是如此,它可以將一個函式的呼叫頻率限制在一定閾值內,例如 1s,那麼 1s 內這個函式一定不會被呼叫兩次

「中高階前端面試」JavaScript手寫程式碼無敵祕籍

簡單的節流函式:

function throttle(fn, wait) {
	let prev = new Date();
	return function() { 
	    const args = arguments;
		const now = new Date();
		if (now - prev > wait) {
			fn.apply(this, args);
			prev = new Date();
		}
	}
複製程式碼

9.3 結合實踐

通過第三個引數來切換模式。

const throttle = function(fn, delay, isDebounce) {
  let timer
  let lastCall = 0
  return function (...args) {
    if (isDebounce) {
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
        fn(...args)
      }, delay)
    } else {
      const now = new Date().getTime()
      if (now - lastCall < delay) return
      lastCall = now
      fn(...args)
    }
  }
}
複製程式碼

10. 手寫一個JS深拷貝

有個最著名的乞丐版實現,在《你不知道的JavaScript(上)》裡也有提及:

「中高階前端面試」JavaScript手寫程式碼無敵祕籍

10.1 乞丐版

 var newObj = JSON.parse( JSON.stringify( someObj ) );
複製程式碼

10.2 面試夠用版

function deepCopy(obj){
    //判斷是否是簡單資料型別,
    if(typeof obj == "object"){
        //複雜資料型別
        var result = obj.constructor == Array ? [] : {};
        for(let i in obj){
            result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
        }
    }else {
        //簡單資料型別 直接 == 賦值
        var result = obj;
    }
    return result;
}
複製程式碼

關於深拷貝的討論天天有,這裡就貼兩種吧,畢竟我...

「中高階前端面試」JavaScript手寫程式碼無敵祕籍

11.實現一個instanceOf

function instanceOf(left,right) {
    let proto = left.__proto__;
    let prototype = right.prototype
    while(true) {
        if(proto == null) return false
        if(proto == prototype) return true
        proto = proto.__proto__;
    }
}

複製程式碼

求一份深圳的內推

目前本人在準備跳槽,希望各位大佬和HR小姐姐可以內推一份靠譜的深圳前端崗位!996.ICU 就算了。

「中高階前端面試」JavaScript手寫程式碼無敵祕籍

「中高階前端面試」JavaScript手寫程式碼無敵祕籍

  • 微信:huab119
  • 郵箱:454274033@qq.com

作者掘金文章總集

公眾號

「中高階前端面試」JavaScript手寫程式碼無敵祕籍

相關文章