對於 Function.call()的深入理解

AJie發表於2019-03-29

Function 作為 JavaScript 的內建物件,擁有以下兩個方法:

  • Function.call();
  • Function.apply();

這兩個方法所實現的功能都是相同的:將函式作為對想象的方法呼叫。(引用自《JavaScript 權威指南》P.768 Function.call())。這兩者的不同之處在於引數的型別不一樣。

具體二者不同之處不是本文的重點,讀者若想了解可以自行搜尋。


言歸正傳,本文將通過實現類似 Function.call()的功能函式來深入解釋其函式內部的執行機制!

Function.call()的需求分析

在研究該函式內部的執行機制之前,我們先來了解以下該函式具體是要實現什麼樣的功能?

根據《JavaScript 權威指南》P.768 有關 Function.call(thisobj, args...)的描述,其詳細解釋如下:

call()將指定的函式 function 作為物件 thisobj 的方法來呼叫,並傳入引數列表中 thisobj 之後的引數。返回的是呼叫 function 的返回值。在函式體內,關鍵字 this 指代 thisobj 物件,如果 thisobj 位 null,則使用全域性物件。

也就是說 call()函式將會用 thisobj 來呼叫指定的函式 function,並返回結果。

Function.prototype.myCall()方法設計

在有了上述描述之後,我們就可以著手設計方法了!我們先寫一下虛擬碼:

Function.prototype.myCall = function(obj) {
    let object = 如果有傳入物件obj傳入則保留obj,否則為全域性物件
    在object上新增一個臨時的屬性fn
    將this賦值給fn	//讓this所指向的物件(函式的呼叫者)成為object的一個屬性。
    傳參給object.fn(),並且執行該方法,將返回值保留。
    刪除object上的臨時屬性fn。
    返回之前保留的保留值。
}
複製程式碼

Function.prototype.myCall()的實現

有了之前的設計之後,就是依葫蘆畫瓢,將虛擬碼進行實現。具體實現如下:

Function.prototype.myCall = function(obj) {
  let object = obj || global; // node環境中的全域性物件
  object.fn = this;
  var args = [];
  for (let i = 1, len = arguments.length; i < len; i++) {
    args.push("arguments[" + i + "]");
  }
  let result = eval("object.fn(" + args + ")");
  delete object.fn;
  return result;
};
複製程式碼

Function.prototype.myCall()的測試

這裡將會用列出兩個例子,一方面是為了測試 myCall()的功能實現,另一方面是為了加深對於 call()函式的理解!

  1. 案例一是一個簡單的函式呼叫,即用 fn2()這個函式物件,呼叫 fn1(a)這個方法,具體程式碼如下:

    function fn1(a) {
      console.log(a);
    }
    
    function fn2() {
      console.log("*");
    }
    
    fn1.myCall(fn2, "HelloWorld!"); //輸出HelloWorld!
    複製程式碼

    在這段程式碼 中,最終我們是要執行 fn1.myCall(fn2, "HelloWorld!")。

    在執行這段程式碼的時候,myCall()函式中的 this 指向呼叫者 fn1。myCall()的第一個引數接收了 arguments 的第一個引數,也就是 fn2,並且賦給了 object 變數。我們將 this 所指向的物件新增為 objec 的臨時屬性 fn。之後執行 object.fn()函式,實際上就是執行了臨時新增在 object 上的 this 物件,也就是這裡的 fn1。我們將傳入的 arguments 從第二個開始傳入到 object.fn()中並且執行,將返回值保留。最後銷燬臨時屬性 fn,返回保留值。

  2. 案例二是 myCall 的連環呼叫,具體程式碼如下:

    function fn1() {
      console.log(1);
    }
    function fn2() {
      console.log(2);
    }
    
    fn1.myCall.myCall(fn2); //輸出2
    複製程式碼

    這段程式碼理解起來說複雜也不復雜,關鍵是要理解是誰在呼叫誰的關係。

    • 首先函式從左往右進行執行,先查詢fn1.myCall.myCall(fn2);中的粗體部分。

      在執行前半段的時候,實際上是一個物件屬性查詢的過程,最終依據原型鏈(在這裡預設讀者明白什麼是原型鏈了,如果不清楚原型鏈,請自行查詢資料)查詢到存在在原型鏈中的 Function.prototype.myCall 屬性,其實際就是一個方法。所以上述程式碼實際上可以與另一句程式碼相等:

      //這兩句程式碼的意思是一樣的!
      fn1.myCall.myCall(fn2);
      Function.prototype.myCall.myCall(fn2);
      複製程式碼
    • 這裡我們就直接用 Function.prototype.myCall.**myCall(fn2)**進行解釋。

      第二步將執行後半段粗體部分。

      • 先將 fn2 賦給臨時變數 object。
      • this 所指向的物件就是一個函式物件:Function.prototype.myCall()。所以賦給 object.fn 臨時屬性指向的就是 Function.prototype.myCall()方法。
      • 由於 arguments 長度為 1,所以直接執行 object.fn()方法,也就是 object.myCall(),也就是 fn2.myCall()
      • fn2.myCall()實際上執行的是 global.fn2(),具體執行過程就不再熬述了,故輸出了 2,沒有返回值。
      • 銷燬臨時屬性 fn。
      • 函式執行完畢。

      這裡我們將一些幫助理解的測試程式碼給大家貼上一下:

      fn1.myCall.myCall(fn2); //輸出2
      Function.prototype.myCall.myCall(fn2); //輸出2
      fn2.myCall(); //輸出2
      複製程式碼

2019/03/29

AJie

相關文章