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