給 Number 打 Call

最近「打 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;
複製代碼

結果初探

打 call 的結果

不知道你們有沒有猜中運行結果呢?反正我看到這個結果的時候是崩潰的(心裏活動:爲何相等的三個函數,對相同的參數,返回的結果卻不同??不愧是 JavaScript!)不過雖然結果看起來違反直覺,可是從上述代碼中仍是能夠得出兩個好消息的:java

  1. 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)
    複製代碼
  2. Number.callNumber.call.callNumber.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

myCall 執行結果

運行的結果和原生的 call 函數相同,說明這個簡單版的 myCall 函數實現了目標。在上圖中,綠塊和藍塊的輸出相同,能夠把它們倆兒歸爲一類,因此接下來主要對紅塊和綠塊中的結果進行分析。

Number.myCall

從紅塊中,咱們能夠看出函數調用只發生了一次,且 this 爲 Number,obj 爲 Number,arguments 爲 一、2 和 3,因此 Number.myCall(Number, 1, 2, 3) 其實至關於調用了 Number.bind(Number)(1, 2, 3),因此最後結果爲 1,這與咱們在第一節得出的結論相同。

Number.myCall.myCall

從綠塊中,咱們能夠看出函數調用發生了兩次:

  1. 對於第一次調用,將輸出的 this、obj 和 arguments 帶入到 myCall 中,能夠發現實際上是執行了 myCall.bind(Number)(1, 2, 3)
  2. 對於第二次調用,一樣將輸出的 this、obj 和 arguments 帶入到 myCall 中,能夠發現實際上是執行了 Number.bind(1)(2, 3),因此結果與執行 Number(2, 3)相同,也就是 2。

———————————— 前方高能預警!!會比較繞!!請準備!請準備!————————————

對比 Number.myCallNumber.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)
}
複製代碼

博客原文連接

相關文章
相關標籤/搜索