1. 程式人生 > >apply call bind的用法與實現

apply call bind的用法與實現

### 概念 `apply call 和bind` 允許為不同的物件分配和呼叫屬於一個物件的函式/方法。同時它們可以改變函式內 this 的指向。 #### 區別 - apply 和 call 接收的引數形式不同 - apply 和 call 都是直接呼叫函式並得到函式執行結果,而 bind 會返回待執行函式,需要再次呼叫 #### 用法演示 我們先建立一個物件 parent ```js const parent = { name: 'parent', sayPerson (age, addr) { return { name: this.name, age, addr } } } ``` 顯然它具有 name 屬性,及方法 sayPerson,我們現在可以通過 `parent.sayPerson()` 來輸出該物件資訊。 ```js const person = parent.sayPerson(60, 'shenzhen'); // {name: "parent", age: 60, addr: "shenzhen"} ``` 現在我們再建立一個物件 son ```js const son = { name: 'son' } ``` 我們現在也想得到 son 的資訊,但是 son 物件沒有 sayPerson 函式怎麼辦?藉助已有的 parent 物件和 call 方法,我們可以這樣寫 ```js const person = parent.sayPerson.call(son, 26, 'shenzhen'); // {name: "son", age: 26, addr: "shenzhen"} ``` 可以看出,通過呼叫 call 函式,我們為 son 物件分配了 sayPerson 方法並進行呼叫。實現了一個物件可以呼叫不屬於它自己的方法,並且函式內的 this 指向該物件。apply 方法的用法其實一樣,只是傳參有些區別 ```js const person = parent.sayPerson.call(son, [26, 'shenzhen']); // {name: "son", age: 26, addr: "shenzhen"} ``` bind 函式則不直接呼叫函式,而是返回待呼叫函式 ```js const sayPersonFn = parent.sayPerson.bind(son, 26, 'shenzhen'); const person = sayPersonFn(); // {name: "son", age: 26, addr: "shenzhen"} ``` 以上就是三者的使用方法和區別,下面我們來看看它們是如何實現的 ### 實現 #### call的實現 實現原理就是為物件 obj 新增需要呼叫的方法,接著呼叫該方法(此時 this 指向 obj),呼叫過後再刪除該方法 簡單版 ```js Object.prototype.callFn = function (...args) { // 第一個引數為目標物件 const context = args[0]; args.shift(); // 為物件賦值需要呼叫的方法 context.fn = this; // 呼叫該方法 context.fn(...args); // 刪除方法 delete context.fn; } ``` 加上返回值 ```js Object.prototype.callFn = function (...args) { // 第一個引數為目標物件 const context = args[0]; args.shift(); // 為物件賦值需要呼叫的方法 context.fn = this; // 呼叫該方法 const result = context.fn(...args); // 刪除方法 delete context.fn; return result; } ``` 在測試中發現,我們呼叫 call,如果第一個引數是 null 或者 undefined,那麼 call 將以全域性 window 來呼叫方法,此時 this 也指向 window。如果第一個引數不是物件型別,則以空物件 {} 來呼叫方法。 ```js Object.prototype.callFn = function (...args) { // 第一個引數為目標物件 let context = args[0]; // undefined 和 null 指向 window if (context === null || context === undefined) { context = window; } // 不是物件型別則建立空物件 if (typeof context !== 'object') { context = {}; } args.shift(); // 為物件賦值需要呼叫的方法 context.fn = this; // 呼叫該方法 const result = context.fn(...args); // 刪除方法 delete context.fn; return result; } ``` 至此,我們實現了一個完整的 call 方法。 #### apply的實現 既然和 call 只存在傳參的區別,那我們只需要簡單修改下已實現的 call 方法即可。 ```js Object.prototype.applyFn = function (...args) { let context = args[0]; if (context === null || context === undefined) { context = window; } if (typeof context !== 'object') { context = {}; } args.shift(); context.fn = this; // 和 call 存在差異的地方 const result = context.fn(...args[0]); delete context.fn; return result; } ``` #### bind的實現 在實現了 apply 和 call 的前提下,bind 的實現也比較簡單。 ```js Object.prototype.bindFn = function (...args) { // 實際就是多包了層待執行函式 return () => { return this.applyFn(args[0], (args || []).slice(1)); } } ``` 至於以 bind 方法返回的函式作為建構函式來建立物件會存在的問題請參考[JavaScript深入之bind的模擬實現](https://github.com/mqyqingfeng/Blog/issues/12)。 ### 總結 `call apply bind` 在工作中實際上是比較常見的函式,特別是在一些框架或庫的原始碼中,但是經常有人會混淆它們的用法。希望大家通過此篇文章可以徹底弄清它們的作用與區別,並且知道其實現原理,知其然知其所以然。 ### 參考 - [JavaScript深入之call和apply的模擬實現](https://github.com/mqyqingfeng/Blog/issues/11) - [JavaScript深入之bind的模擬實現](https://github.com/mqyqingfeng/Blog/issues/12) *** 歡迎到前端學習打卡群一起學習~51