最近「打 call」這個詞忽然流行起來,我想到 call
在 JavaScript 中但是個重要的方法,那麼能不能用 JavaScript 也來玩一把打 call 呢?因而我在 Number 上實驗了下,寫出來的代碼讓我本身都一臉懵逼了,你們能猜到下面這段代碼的輸出結果嗎?javascript
Number.call(Number, 1, 2, 3);
Number.call.call(Number, 1, 2, 3);
Number.call.call.call(Number, 1, 2, 3);
Number.call === Number.call.call;
Number.call === Number.call.call.call;
複製代碼
不知道你們有沒有猜中運行結果呢?反正我看到這個結果的時候是崩潰的(心裏活動:爲何相等的三個函數,對相同的參數,返回的結果卻不同??不愧是 JavaScript!)不過雖然結果看起來違反直覺,可是從上述代碼中仍是能夠得出兩個好消息的:java
Number.call(Number, 1, 2, 3)
這段代碼的運行結果是符合預期的,它其實就是將 Number 中的 this 指向 Number,而後執行 Number(1, 2, 3),和直接執行 Number(1, 2, 3)
做用相同,效果就是對傳入的第一個參數嘗試轉化爲數字並返回,咱們能夠經過下列代碼驗證一下:git
Number.call(Number, 1, 2, 3); // output is 1
Number(1, 2, 3); // output is 1
Number.call(Number, '1', '2', '3'); // output is 1
Number('1', '2', '3'); // output is 1
Number.call(Number, 'do', 'call'); // output is NaN(Not a Number)
Number('do', 'call'); // output is NaN(Not a Number)
複製代碼
Number.call
、Number.call.call
和 Number.call.call.call
用全等符號的判斷結果是 true
,這說明它們是同一個函數, 那麼它們究竟是指向哪個函數呢?仔細想一下,Number
是函數,Number.call
是函數,Number.call.call
也是函數,那麼訪問一個函數的 call
方法會不會就是訪問 Function 原型上的 call 方法呢?一樣地,咱們經過下列代碼驗證一下:github
typeof Number; // output is "function"
typeof Number.call; // output is "function"
typeof Number.call.call; // output is "function"
Number.call === Function.prototype.call; // output is true
Number.call.call === Function.prototype.call; // output is true
Number.call.call.call === Function.prototype.call; // output is true
複製代碼
關於原型鏈更深刻的知識,能夠參考我以前寫的博客 - __proto__ 與 prototype 的關係segmentfault
雖然有了一些收穫,可是爲何對於相同的函數 Function.prototype.call
,輸入相同的參數 (Number, 1, 2, 3)
,有時候獲得的結果是 1,有時候獲得結果倒是 2 呢?app
既然已經定位到了 Function.prototype.call
這裏,那麼咱們能夠本身寫一個 myCall
函數來代替 call
函數,從而進一步地探究這個函數內部發生了什麼。不要被本身寫一個 call
函數嚇到,畢竟它的做用很簡單:將調用者函數中的 this 改成傳入的第一個參數,而後再依次傳入後面的參數並執行函數。因此咱們能夠先編寫一個這樣的函數:函數
Function.prototype.myCall = function myCall(obj, ...args) {
console.log(`this is ${this.name}, obj is ${obj}, arguments are ${args}`);
console.log('------------------');
const newFunc = this.bind(obj);
return newFunc(...args);
}
複製代碼
在上面的代碼中,關鍵在於 const newFunc = this.bind(obj);
,這裏的 this
指的就是調用者函數,而後經過 this.bind(obj)
將調用者函數中的 this(注意:這個 this 是調用者函數中的 this,而不是 myCall 中的 this) 變爲了 obj。工具
上面只是爲了探究問題而實現的簡單版 call 函數,實際上 call 函數還會對傳入的第一個參數作校驗和替換,例如傳入基本數據類型(primitive values)時會被轉化爲對象,不過這些與本次問題無關,能夠不用關注 ^_^ui
而後讓咱們來運行一下 myCall,看看結果如何:this
運行的結果和原生的 call 函數相同,說明這個簡單版的 myCall 函數實現了目標。在上圖中,綠塊和藍塊的輸出相同,能夠把它們倆兒歸爲一類,因此接下來主要對紅塊和綠塊中的結果進行分析。
從紅塊中,咱們能夠看出函數調用只發生了一次,且 this 爲 Number,obj 爲 Number,arguments 爲 一、2 和 3,因此 Number.myCall(Number, 1, 2, 3)
其實至關於調用了 Number.bind(Number)(1, 2, 3)
,因此最後結果爲 1,這與咱們在第一節得出的結論相同。
從綠塊中,咱們能夠看出函數調用發生了兩次:
myCall.bind(Number)(1, 2, 3)
;Number.bind(1)(2, 3)
,因此結果與執行 Number(2, 3)
相同,也就是 2。———————————— 前方高能預警!!會比較繞!!請準備!請準備!————————————
對比 Number.myCall
和 Number.myCall.myCall
的第一次調用,能夠發現其實 obj 和 arguments 都沒有變化,只是 this 的指向發生了變化:對於 Number.myCall
而言,myCall 的調用者是 Number
,因此 this 指向了 Number;對於 Number.myCall.myCall
而言, 最後一個 myCall 的調用者是 Number.myCall
,因此 this 指向了 myCall。再進一步,咱們看看 Number.myCall.myCall.myCall
,最後一個 myCall 的調用者是 Number.myCall.myCall
,也就是 Funct.prototype.myCall,因此 this 指向了 myCall,和 Number.myCall.myCall 調用時 this 指向相同,因此輸出的結果也相同,都是 2。
———————————————————— 預警結束~~~ ————————————————————
因此說呀,儘管 Number.myCall 和 Number.myCall.myCall 是同一個函數,並且傳入的參數也相同,可是因爲函數內部的 this 指向不一樣,致使了函數最後的執行結果不一樣。這就是基於 「原型鏈」 的動態語言 JavaScript 的「神奇」之處,在基於類的語言如 C++、Java 中但是難以碰到這等好玩的事情呢~
若是讀者堅持看到了這裏並且尚未暈的話,我真心地佩服你 👍👍 想當初我看到這個執行結果,到最後弄明白的一個多小時裏,我是一直處於懵逼的狀態中。這裏特別感謝和我一塊兒探究的 avwo(他出品的代理工具 whistle 廣受好評,你們能夠試試~) 和提供文章題目的 litten。
最後爲了不你們以爲我是瘋子竟然寫出這種代碼來,我在這裏說明一下,之因此研究這個是由於看到下面一段代碼,你們能夠猜猜它的做用是什麼(應該有很多人都見過~):
function arrayGenerator(length) {
return Array.apply(null, { length: length }).map(Number.call, Number)
}
複製代碼