從【預編譯】到【宣告提升】到【作用域鏈】再到【閉包】

海洋餅乾發表於2022-01-19

JavaScript引擎在程式碼執行之前會先進行如下操作:

  1. 先進行分詞/詞法分析將語句分割成詞法單元 token,在對當前的整個作用域分析完成後,JS引擎會將 token進行解析/語法分析翻譯成 AST(抽象語法樹)
  2. 預編譯(預處理)
  3. 邊解釋邊執行(不是純解釋,還有JIT編譯,這裡不展開了)

預編譯(預處理)

有人說JavaScript沒有預編譯,是屬於語法分析的一部分,有人說有詞法,語法和程式碼生成就已經屬於編譯了,只是不需要提前編譯的,而是在執行前的幾毫秒才編譯。所以JavaScript到底有沒有預編譯或者這些步驟屬不屬於預編譯(預處理)大佬們的意見也不同,我們就不探討了,暫且把語法分析生成AST後到執行前的一些處理過程稱之為“ 預編譯 ”把。

  1. 全域性預編譯

    1. 建立 Global Object 全域性上下文物件
    2. 找變數宣告,將變數名繫結為GO的一個屬性,值為undefined
    3. 找函式宣告,將函式名繫結為GO的一個屬性,值為函式體
    4. 執行全域性程式碼
  2. 區域性預編譯

    1. 建立 Activation Object 函式上下文物件
    2. 找行參,將行參名繫結為AO的一個屬性,值為 undefined
    3. 找變數宣告,將變數名繫結為AO的一個屬性,值為 undefined
    4. 實參值賦值給形參
    5. 找函式宣告,將函式名繫結為AO的一個屬性,值為 函式體
    6. 函式生成[[scope]]屬性,值為一個陣列,陣列的第一項是本函式的AO,下一項為外層函式的AO,一直往外層,直到最後一項為GO
    7. 執行函式程式碼

宣告提升,作用域鏈

上面預編譯的前半部分就是所謂的宣告提升,我有文章單獨講了宣告提升就不贅述了,詳情點選這裡

函式執行前會生成`[[scope]]`屬性,值為一個陣列,陣列的第一項是本函式的 AO
下一項為外層函式的 AO,一直往外層,直到最後一項為 GO

AO是函式的作用域,而[[scope]]這樣的鏈式陣列則是函式的作用域鏈,當函式進行值查詢RHS時會先在[[scope]]中的第一項中尋找,如果沒有找到則查詢第二層,以此類推直到查詢到GO為止。

閉包

函式FN執行結束後,這個函式FNAO的生命週期就應該結束了,需要被銷燬。但是有特殊情況,雖然說是特殊情況,但是開發中非常常見,就是函式FN執行結束後return了一個內部函式A並被外部儲存後,本來應該被銷燬的函式FNAO便無法被銷燬,因為函式A的[[scope]]儲存著函式FNAO作為自己原型鏈的一部分,只要儲存函式A的變數不被銷燬或者變數所在的AO不被銷燬則函式FNAO永遠不會被銷燬,並且變數儲存的函式A可以一直訪問函式FN中的變數和值,這樣的現象叫做閉包,這樣的閉包大量出現會導致記憶體洩漏或者載入過慢。

var Variable_1 = 1;
function FN() {
    var Variable_2 = 2;
    function A() {
        var Variable_3 = 3
        console.log(Variable_2);
    }
    return A;
}
var Function_A = FN();
FnVariable(); // 接受 函式A 的變數 Function_A 通過呼叫可以輸出 FN 中的 Variable_2

相關文章