實現 call()、apply() 和 bind() 方法
現在在看新東西的時候,經常會很自然地去思考其內部實現機制,我覺得這個是通向進階之路的一個很好的思維方式。
我們平時經常會使用到 call()、apply() 以及 bind() 方法,那麼你是否清楚這幾個方法的內部實現機制呢?在這篇博文中我希望能夠通過實現自己的 call()、apply() 和 bind() 方法以使我們能夠更好地理解其內部實現機制。
Function.prototype.call
使用過 call() 方法的童鞋應該都知道,call() 方法的作用就是執行呼叫函式並改變其內部的 this 指向。
在 MDN 中的定義是: call() 方法呼叫一個函式, 其具有一個指定的 this 值和分別地提供的引數(引數的列表) 。
讓我們看如下例子:
// 瀏覽器環境中執行 let count = 0; let obj = { count:1 } function addNum(arg1,arg2){ return this.count + arg1 + arg2; } console.log(addNum(2,3)) // 5 console.log(addNum.call(obj,2,3)) // 6
addNum.call(obj) 執行 addNum() 方法並將其內部 this 指向改為 obj,所以最終返回的是 obj.count + arg1 + arg2 。
那麼如何將 addNum() 方法內部 this 指向改為 obj 呢?如果對 this 指向有一定了解的同學,很容易可以想到如下的方式:
obj.fn = addNum; console.log(obj.fn(2,3)); // 6
事實上,call() 方法內部改變 this 指向的機制也是一樣的, 通過將呼叫函式作為傳入物件的一個屬性來呼叫,來實現 this 指向的改變。
所以接下來,我們便可以通過這個機制來實現 call() 方法,其需要有以下特性:
- 不傳引數或者第一個引數傳 null,this 指向 window;
- 第一個引數之後的引數作為呼叫函式的傳參接收;
- 改變函式 this 指向,返回呼叫函式執行結果;
所以,最終實現的 myCall() 方法如下:
Function.prototype.myCall = function (context) { if (typeof this !== 'function') throw new TypeError('Error'); context = context || window context.fn = this const args = [...arguments].slice(1) const result = context.fn(...args) delete context.fn return result }
通過 myCall() 方法來實現上述例子依舊能得到正確的返回結果:
console.log(addNum.myCall(obj,2,3)) // 6
Function.prototype.apply
apply() 方法與 call() 方法類似,區別在於 apply() 方法在接收呼叫函式引數的時候是以陣列的形式接收的,所以在對引數的處理時會有所不同。
在 MDN 中的定義是: apply() 方法呼叫一個具有給定 this 值的函式,以及作為一個數組(或類似陣列物件)提供的引數。
同樣的,最終實現的 myApply() 方法如下:
Function.prototype.myApply = function (context) { if (typeof this !== 'function') throw new TypeError('Error'); context = context || window context.fn = this let result if (arguments[1]) { result = context.fn(...arguments[1]) } else { result = context.fn() } delete context.fn return result }
通過 myApply() 方法來實現上述例子依舊能得到正確的返回結果:
console.log(addNum.myApply(obj,[2,3])) // 6
Function.prototype.bind
bind() 方法相較之前的兩個函式則要複雜一些。
在 MDN 中的定義是: bind() 方法建立一個新的函式,在呼叫時設定 this 關鍵字為提供的值,並在呼叫新函式時,將給定引數列表作為原函式的引數序列的前若干項。
如下例子:
let bindFn1 = addNum.bind(obj); console.log(bindFn1(2,3)); // 6 let bindFn2 = addNum.bind(obj, 2); console.log(bindFn2(3)); // 6 let bindFn3 = addNum.bind(obj, 2, 3); console.log(bindFn3()); // 6
所以我們實現的 bind() 方法需要有以下特性:
- 返回一個函式,該函式可以直接呼叫也可以通過 new 方式呼叫;
- 直接呼叫則改變函式 this 指向,通過 new 方式呼叫則忽略;
- 返回函式能接收 bind 函式傳遞的部分引數;
所以,最終實現的 myBind() 方法如下:
Function.prototype.myBind = function (context) { if (typeof this !== 'function') throw new TypeError('Error'); const _this = this const args = [...arguments].slice(1) return function F() { if (this instanceof F) {//通過 new 方式呼叫的情況 return new _this(...args, ...arguments) } return _this.apply(context, args.concat(...arguments)) } }
通過 myBind() 方法來實現上述例子依舊能得到正確的返回結果:
let bindFn1 = addNum.myBind(obj); console.log(bindFn1(2,3)); // 6 let bindFn2 = addNum.myBind(obj, 2); console.log(bindFn2(3)); // 6 let bindFn3 = addNum.myBind(obj, 2, 3); console.log(bindFn3()); // 6
公眾號不定時分享個人在前端方面的學習經驗,歡迎關注。