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()函式的理解!
-
案例一是一個簡單的函式呼叫,即用 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,返回保留值。
-
案例二是 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