在JavaScript
中,如果想要改變當前函式呼叫的上下文物件的時候,我們都會聯想到call、apply和bind
。比如下面?
var name = 'window name';
var obj = {
name: 'call_me_R'
};
function sayName(){
console.log(this.name);
}
sayName(); // window name
sayName.call(obj); // call_me_R
複製程式碼
那麼,call, apply和bind
有什麼區別呢?
call,apply和bind的區別
在說區別之前,先簡單的說下三者的共同之處吧:
- 都是用來改變函式的this物件的指向
- 第一個引數都是this要指向的物件
- 都可以利用後續引數進行傳參
下面說下區別:
引數的傳遞
call
方法傳參是傳一個或者是多個引數,第一個引數是指定的物件,如開篇的obj
。
func.call(thisArg, arg1, arg2, ...)
複製程式碼
apply
方法傳參是傳一個或兩個引數,第一個引數是指定的物件,第二個引數是一個陣列或者類陣列物件。
func.apply(thisArg, [argsArray])
複製程式碼
bind
方法傳參是傳一個或者多個引數,跟call
方法傳遞引數一樣。
func.bind(this.thisArg, arg1, arg2, ...)
複製程式碼
簡言之,call
和bind
傳參一樣;apply
如果要傳第二個引數的話,應該傳遞一個類陣列。
呼叫後是否立執行
call和apply在函式呼叫它們之後,會立即執行這個函式;而函式呼叫bind之後,會返回撥用函式的引用,如果要執行的話,需要執行返回的函式引用。
變動下開篇的demo
程式碼,會比較容易理解:
var name = 'window name';
var obj = {
name: 'call_me_R'
};
function sayName(){
console.log(this.name);
}
sayName(); // window name
sayName.call(obj); // call_me_R
sayName.apply(obj); // call_me_R
console.log('---divided line---');
var _sayName = sayName.bind(obj);
_sayName(); // call_me_R
複製程式碼
在筆者看來,call, apply 和 bind
的區分點主要是上面的這兩點,歡迎有想法的讀者進行補充~?
手寫call, apply, bind方法
這裡是簡單的實現下相關方法的封裝,為了簡潔,我這裡儘量使用了ES6的語法進行編寫,詳細的參考程式碼可以直接戳airuikun大牛的airuikun/Weekly-FE-Interview issues。
call方法實現
在上面的瞭解中,我們很清楚了call
的傳參格式和呼叫執行方式,那麼就有了下面的實現方法:
Function.prototype.call2 = function(context, ...args){
context = context || window; // 因為傳遞過來的context有可能是null
context.fn = this; // 讓fn的上下文為context
const result = context.fn(...args);
delete context.fn;
return result; // 因為有可能this函式會有返回值return
}
複製程式碼
我們來測試下:
var name = 'window name';
var obj = {
name: 'call_me_R'
};
// Function.prototype.call2 is here ...
function sayName(a){
console.log(a + this.name);
return this;
}
sayName(''); // window name
var _this = sayName.call2(obj, 'hello '); // hello call_me_R
console.log(_this); // {name: "call_me_R"}
複製程式碼
apply方法實現
apply
方法和call
方法差不多,區分點是apply
第二個引數是傳遞陣列:
Function.prototype.apply2 = function(context, arr){
context = context || window; // 因為傳遞過來的context有可能是null
context.fn = this; // 讓fn的上下文為context
arr = arr || []; // 對傳進來的陣列引數進行處理
const result = context.fn(...arr); // 相當於context.fn(arguments[1], arguments[2], ...)
delete context.fn;
return result; // 因為有可能this函式會有返回值return
}
複製程式碼
同樣的,我們來測試下:
var name = 'window name';
var obj = {
name: 'call_me_R'
};
// Function.prototype.apply2 is here ...
function sayName(){
console.log((arguments[0] || '') + this.name);
return this;
}
sayName(); // window name
var _this = sayName.apply2(obj, ['hello ']); // hello call_me_R
console.log(_this); // {name: "call_me_R"}
複製程式碼
bind方法實現
bind
的實現和上面的兩種就有些差別,雖然和call
傳參相同,但是bind
被呼叫後返回的是呼叫函式的指標。那麼,這就說明bind
內部是返回一個函式,思路開啟了:
Function.prototype.bind2 = function(context, ...args){
var fn = this;
return function () { // 這裡不能使用箭頭函式,不然引數arguments的指向就很尷尬了,指向父函式的引數
fn.call(context, ...args, ...arguments);
}
}
複製程式碼
我們還是來測試一下:
var name = 'window name';
var obj = {
name: 'call_me_R'
};
// Function.prototype.bind2 is here ...
function sayName(){
console.log((arguments[0] || '') + this.name + (arguments[1] || ''));
}
sayName(); // window name
sayName.bind2(obj, 'hello ')(); // hello call_me_R
sayName.bind2(obj, 'hello ')('!'); // hello call_me_R!
複製程式碼
美滋滋?,成功地簡單實現了call、apply和bind
的方法,那麼你可能會對上面的某些程式碼有疑問❓
疑惑點
1. 問:call中為什麼說 context.fn = this; // 讓fn的上下文為context 呢?
答:
我們先來看看下面這段程式碼--
var name = 'window name';
var obj = {
name: 'call_me_R',
sayHi: function() {
console.log('Hello ' + this.name);
}
};
obj.sayHi(); // Hello call_me_R
window.fn = obj.sayHi;
window.fn(); // Hello window name
複製程式碼
嗯,神奇了一丟丟,操作window.fn = obj.sayHi;
改變了this
的指向,也就是this
由指向obj
改為指向window
了。
簡單來說:this的值並不是由函式定義放在哪個物件裡面決定的,而是函式執行時由誰來喚起來決定的。
2. 問:bind中返回的引數為什麼是傳遞(context, ...args, ...arguments), 而不是(context, ...args)呢?
答:
這是為了包含返回函式也能傳參的情況,也就是bind()()
中的第二個括號可以傳遞引數。
call和apply哪個好?
據調查--call和apply的效能對比,在分不同傳參的情況下,call的效能是優於apply的。不過在現代的高版本瀏覽器上面,兩者的差異並不大。
而在相容性方面,兩者都好啦,別說IE了哈。
在使用的方面還是得按照需求來使用call和apply
,畢竟技術都在更新。適合業務的就是最好的~囧
後話
客官可以star下github的博文倉庫否,歡迎提意見共同成長啊~?
參考
airuikun/Weekly-FE-Interview issues
《JavaScript高階程式設計》