問題復現
有這麼一道題:
function fn1(){
console.log(`fn1 ${this}`)
}
function fn2(){
console.log(`fn2 ${this}`)
}
fn1.call(fn2);// fn1 function fn2(){console.log(`fn2 ${this}`)}
fn1.call.call(fn2);// fn2 [object Window]
fn1.call.call.call.call(fn2);// fn2 [object Window]
問題分析
關於call我們一般知道這些:
- 主要作用是改變函數的
this指向 - 傳參形式類似
function.call(thisArg, arg1, arg2, ...) -
執行步驟
- 改變調用函數的
this指向 - 傳參並將調用函數執行
- 改變調用函數的
所以,對於
fn1.call(fn2); // fn1 function fn2(){console.log(`fn2 ${this}`)}
這樣的輸出並不會使我們感到意外,根據上面的已知信息,這段代碼的執行邏輯如下:
將fn1中的this指向fn2然後執行fn1。
而對於
fn1.call.call(fn2);// fn2 [object Window]
這個樣的輸出就會讓人困惑了,那麼我們該如何理解呢?
問題解決
要理解這個輸出我們需要知道這兩點:
key_1: call方法是哪裏來的?
首先call是Function基類原型上的方法,也就是Function.prototype.call。
所以fn1.call.call(fn2)相當於Function.prototype.call.call(fn2)
key_2: call的執行過程是什麼樣的
用偽代碼表示,call的執行過程大概是這樣(不考慮傳參的情況)的
Function.prototype.call = function (context) {
// 1. 首先明確第一個this,因為這是個原型上的方法,是公共的方法
// 所以我們需要知道是誰在調用call這個方法,誰在調用這個this就是誰
// 我給這裏的this起個別名就是this_caller
// 2. 改變this_caller中的this指向context
// 3. 執行this_caller()
this();
}
知道這兩點之後,結合fn1.call.call(fn2)實例分析就是:
- 根據
key_1,我們知道所以,fn1.call.call(fn2)的第一個call是Function.prototype.call,第二個call是通過Function.prototype.call的原型鏈找到的。 -
根據上面得到的信息,我們把可以把
fn1.call.call(fn2)看做(fn1.call).call(fn2)。結合key_2我們可以知道,第二個call起了這些作用:- 首選明確這個
call的this_caller是fn1.call fn1.call裏面的this指向變成了fn2fn1.call的變成了fn2
3.整體來看,就是
fn1.call.call(fn2)變成了fn2() - 首選明確這個
上面的問題關鍵在於:
fn1裏面雖然沒有this,不受call改變this指向的影響,但是fn1.call裏面有this,受call改變this指向的影響。
如果能夠理解這一點的話,那這個問題就比較好理解了。
參考文檔
- JavaScript中的call、apply、bind深入理解
- JavaScript中關於call函數的一道面試題
- Function.prototype.call()