分不清bind、apply、call?手寫實現一下就明白了

語言: CN / TW / HK

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,如果未傳入引數列表,或者第一個引數是nullundefined,那麼新函式的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不同,callapply都是用來執行函式的,可以解決執行的函式的this指向問題。

語法

js function.call(thisArg, arg1, arg2, ...) function.apply(thisArg, argsArray)

call的引數列表是可選的,如果傳入的thisArgnull或者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

callapply無法修改箭頭函式的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()返回的是一個新函式,執行新函式就相當於是通過callapply來呼叫原函式,並傳入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); } ```

注意列印的當前函式以及上下文:

image-20220623164010013.png

實現callapplybind有很大的不同就是如何來處理this繫結。

總結

學會了如何實現bind、call和apply,對於理解如何使用,以及如何避免潛在的錯誤有很大的幫助。特別是callapply,我們在實現的時候藉助於物件內部的非箭頭函式,其this指向物件自身這一基礎知識,實現了this繫結。如果還未搞清楚的朋友,可以將程式碼執行起來看看,也許能幫助你更好的理解。