分不清bind、apply、call?手寫實現一下就明白了
theme: fancy
持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第25天,點選檢視活動詳情
bind、call和apply都是Function
原型鏈上面的方法,因此不管是使用function
宣告的函式,還是箭頭函式都可以直接呼叫。這三個函式在使用時都可以改變this
指向,本文就帶你看看如何實現bind、call和apply。
bind、call和apply的用法
bind
bind()
方法可以被函式物件呼叫,並返回一個新建立的函式。
語法
:
js
function.bind(thisArg[, arg1[, arg2[, ...]]])
bind()
會將第一個引數作為新函式的this
,如果未傳入引數列表,或者第一個引數是null
或undefined
,那麼新函式的this
將會是該函式執行作用域的this
。使用bind()
應注意以下事項:
- 返回一個新的函式,但是不會立即執行該函式
- 根據傳入的引數列表繫結
this
指向,如果未傳入thisArg
,那麼需要明確this的指向 - 如果是箭頭函式,無法改變this,只能改變引數,這一點我們在這些情況下不建議你使用箭頭函式也講到過
舉個例子:
正常使用
js
function fn(a) {
console.log(this, a)
}
const fn1 = fn.bind({x: 100}); // fn1是一個函式,但是並沒有立即執行
fn1(); // {x:100} 100
console.log(fn === fn1); // false,bind返回的是一個新的函式
箭頭函式
js
const fn = (a) => {
console.log(this, a);
}
const fn1 = fn.bind({x: 100}, 100); // 返回一個新的函式fn1,不會執行
fn1(); // window,100 箭頭函式通過bind返回的函式無法修改其this指向
未繫結this,或繫結到null、undefined
js
const fn = (a) => {
console.log(this, a);
}
const fn1 = fn.bind(); // 未繫結
const fn2 = fn.bind(null); // 繫結null
const fn3 = fn.bind(undefined); // 繫結undefined
fn1(); // 繫結到執行作用域,預設為window
fn2(); // 繫結到執行作用域,預設為window
fn3(); // 繫結到執行作用域,預設為window
call&apply
與bind
不同,call
和apply
都是用來執行函式的,可以解決執行的函式的this指向問題。
語法
:
js
function.call(thisArg, arg1, arg2, ...)
function.apply(thisArg, argsArray)
call
的引數列表是可選的,如果傳入的thisArg
是null
或者undefined
,那麼會自動替換為全域性物件;如果是傳入的原始值,則會替換為原始值對應的包裝型別。apply
的用法和call
類似,不同點在於其額外傳入的引數是一個數組或類陣列物件,而call
的額外引數是不確定引數。
舉個栗子:
js
function fn(a, b) {
console.log(this, a, b);
}
fn.call({x: 100}, 10, 20); // {x: 100} 10 20
fn.apply({x: 100}, [10, 20]); // {x: 100} 10 20
call
和apply
無法修改箭頭函式的this指向:
js
const fn = (a, b) => {
console.log(this, a, b);
}
fn.call({x: 100}, 10, 20); // Window 10 20
fn.apply({x: 100}, [10, 20]); // Window 10 20
簡單回顧了以下bind、call、apply的使用,接下來就看看應該如何來實現。
實現bind
根據我們剛剛使用的bind()
,在設計時需要如下考慮:
- 最終返回的是一個新的函式,可通過
function
來宣告 - 需要繫結新函式的this
- 需要繫結執行時的引數,可通過apply或call來實現
實現程式碼
:
```js // 通過原型鏈註冊方法 // context:傳遞的上下文this;bindArgs表示需要繫結的額外引數 Function.prototype.newBind = function (context, ...bindArgs) { const self = this; // 當前呼叫bind的函式物件
// 返回的函式本身也是可以再傳入引數的 return function (...args) { // 拼接引數 const newArgs = bindArgs.concat(args); return self.apply(context, newArgs) } } function fn(a,b) { console.log(this, a, b); } const fn1 = fn.newBind({x: 100}, 10); fn1(20); // {x: 100} 10 20 ```
bind()
返回的是一個新函式,執行新函式就相當於是通過call
或apply
來呼叫原函式,並傳入this和引數。
實現call和apply
在實現bind
的過程中,我們使用了apply
來完成this的繫結,那麼要實現apply
又應該用什麼來繫結this呢?可能會有小機靈鬼發現,好像在apply
中使用call
,在call
中使用apply
也可以完成this繫結。這不就形成了巢狀嘛,不是我們最終想要的。
我們先來
call和apply的應用:
- bind返回一個新的函式,並不會執行;call和apply會立即執行函式
- 繫結this
- 傳入執行引數
舉個栗子:
js
function fn(a, b) {
console.log(this, a, b);
}
fn.call({x: 100}, 10, 20); // {x: 100} 10 20
fn.apply({x: 100}, [10, 20]); // {x: 100} 10 20
call和apply的實現效果是一樣的,都是立即執行函式,不同的是call需要傳入單個或者多個引數,apply可以傳入一個引數陣列。
如何在函式執行時繫結this:
- const obj = {x: 100, fn() {this.x}}
- 執行obj.fn(),此時fn()內部的this指向的就是obj
- 可以藉此實現函式繫結this
使用過Vue的朋友都知道,Vue例項其實就是一個物件,其裡面的方法在呼叫時,this就會指向當前物件。舉個栗子:
js
let obj = {
key: 'key',
getKey: () => {
return this.key;
},
getKey2() {
return this.key;
}
};
obj.getKey(); // this指向window,返回值取決於window中是否有對應的屬性
obj.getKey2(); // this指向obj,返回 'key'
這個例子在這些情況下不建議你使用箭頭函式也是有提及的,感興趣的朋友可以去看看。根據此原理,我們就可以來嘗試給函式繫結this了:某函式呼叫apply
,那麼我們就將這個函式新增到傳入的this物件中(如果未傳入則this為全域性物件,如果傳入的是原始值,則使用其包裝型別),然後使用()
來執行函式,這個時候函式的this指向的就是我們傳入的this了。
實現程式碼:
```js Function.prototype.newCall = function(context, ...args) { if (context == null) context = globalThis; // 如果傳入的上下文是null或者undefined,則使用全域性globalThis,一般指向的就是window if (typeof context !== 'object') context = new Object(context); // 如果是原始型別(數字、字串、布林值等),則使用其包裝型別
const fnKey = Symbol(); // 使用Symbol可確保key值不會重複,避免屬性覆蓋 context[fnKey] = this; // this指向的是當前呼叫newCall的函式
console.log(context[fnKey]); // 列印當前函式以及上下文this console.log(context);
const res = contextfnKey; // 執行函式,函式的this指向為context delete context[fnKey]; // 刪除fn,防止汙染
return res; // 返回結果 } fn.newCall({x: 100}, 10, 20); // {x: 100} 10 20 function fn(a,b) { console.log(this, a, b); } ```
這樣我們就實現了call
,那麼apply
實現類似,只不過傳入的額外引數要變成陣列或類陣列的方式
```js Function.prototype.newCall = function(context, args) { if (context == null) context = globalThis; // 如果傳入的上下文是null或者undefined,則使用全域性globalThis,一般指向的就是window if (typeof context !== 'object') context = new Object(context); // 如果是原始型別(數字、字串、布林值等),則使用其包裝型別
const fnKey = Symbol(); // 使用Symbol可確保key值不會重複,避免屬性覆蓋 context[fnKey] = this; // this指向的是當前呼叫newCall的函式
console.log(context[fnKey]); // 列印當前函式以及上下文this console.log(context);
const res = contextfnKey; // 執行函式,函式的this指向為context delete context[fnKey]; // 刪除fn,防止汙染
return res; // 返回結果 } fn.newCall({x: 100}, 10, 20); // {x: 100} 10 20 function fn(a,b) { console.log(this, a, b); } ```
注意列印的當前函式以及上下文:
實現call
和apply
與bind
有很大的不同就是如何來處理this
繫結。
總結
學會了如何實現bind、call和apply,對於理解如何使用,以及如何避免潛在的錯誤有很大的幫助。特別是call
和apply
,我們在實現的時候藉助於物件內部的非箭頭函式,其this指向物件自身這一基礎知識,實現了this繫結。如果還未搞清楚的朋友,可以將程式碼執行起來看看,也許能幫助你更好的理解。