1. 程式人生 > >JavaScript的作用域鏈

JavaScript的作用域鏈

跟其他語言一樣,變數和函式的作用域揭示了這些變數和函式的搜尋路徑。對於JavaScript而言,理解作用域更加重要,因為在JavaScript中,作用域可以用來確定this的值,並且JavaScript有閉包,閉包是可以訪問外部環境的作用域的。
每一個JavaScript的函式都是Function物件的一個例項,Function物件有一個內部屬性[[Scope]],這個屬性只能被JavaScript的引擎訪問。通過[[Scope]]屬性可以訪問函式的作用域鏈,從而可以搜尋變數和函式,判斷變數和函式位於作用域鏈中的哪一個活動物件中。

簡單的作用域鏈

當一個函式被建立的時候,因為函式是Function物件的一個例項,因此也會有[[Scope]]這個內部屬性,Scope屬性指向一個作用域鏈,作用域鏈中預設至少包含一個全域性物件變數。

function compare(value1, value2){
    if (value1 < value2) {
        return -1;
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}

var result = compare(5, 10);

以上程式碼先定義了一個compare()函式,然後在全域性作用域中呼叫了這個函式。 在建立compare()函式的時候,該函式的作用域鏈如下圖所示:
這裡寫圖片描述

當compare()函式在全域性作用域中被呼叫執行的時候,JavaScript引擎會建立一個執行時上下文(execution context)的內部物件,一個執行時上下文定義了一個函式執行時的環境。函式誒次執行時的執行時上下文都是不同的,因此多次呼叫就會導致多個執行時上下文的建立與銷燬。
每個執行時上下文都有自己的作用域鏈,用於變數和函式這些識別符號的解析。
執行時上下文在函式呼叫執行時被建立,在函式執行完畢的時候被銷燬。在執行時上下文建立的時候,首先會複製該被呼叫函式的[[Scope]]屬性中的物件,然後一個活動物件(作為變數物件使用)會被建立並推入執行時上下文作用域鏈的前端。對於這個例子中compare()函式的執行時上下文而言,其作用域鏈包含兩個變數物件:compare()的活動物件(activation object of compare())與全域性變數物件(global object)。
這裡寫圖片描述


對於簡單的作用域鏈,就是這樣了,但是有閉包的情況會有所不同。

閉包的作用域鏈

//step1: define createComparisonFunction
function createComparisonFunction(propertyName){
    return function(object1, object2){
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        if (value1 < value2) {
            return
-1; } else if (value1 > value2) { return 1; } else { return 0; } }; } //step2: call createComparisonFunction var compare = createComparisonFunction("name"); //step3: call compare var result = compare({name: "Nicholas"}, {name: "Gerg"}); //step4: dereference closure for recycle memory compare = null;

我們分下列幾個步驟來圖解作用域鏈:
step1: 定義createComparisonFunction;
這裡寫圖片描述
在建立createComparisonFunction函式之後,createComparisonFunction可以被呼叫了,因此一個createComparisonFunction的Function物件被保留下來;
此時記憶體中保留物件:

  1. Global Object
  2. createComparisonFunction物件 & Scope Chain

step2: 執行createComparisonFunction;
這裡寫圖片描述
在執行createComparisonFunction的過程中,首先會建立createComparisonFunction的執行時上下文物件 + ScopeChain + 活動物件,其次會建立一個閉包(匿名函式),
函式執行時記憶體中保留物件:

  1. Global Object
  2. createComparisonFunction的Function物件 + Scope Chain
  3. createComparisonFunction的執行時上下文物件 + Scope Chain
  4. createComparisonFunction的活動物件
  5. Closure(anonymous)的Function物件 + Scope Chain

在執行完createComparisonFunction之後,createComparisonFunction的執行時上下文物件+ScopeChain會被銷燬,但是createComparisonFunction的活動物件因為被Closure(anonymous)物件的ScopeChain所引用,因此不會被銷燬。
函式執行完記憶體中保留物件:

  1. Global Object
  2. createComparisonFunction的Function物件 + Scope Chain
  3. Closure(anonymous)的Function物件 + Scope Chain
  4. createComparisonFunction的活動物件

對比step1, step2在執行完之後,增加了兩個物件:

  1. Closure(anonymous)的Function物件 + Scope Chain
  2. createComparisonFunction的活動物件

這個是因為執行createComparisonFunction所產生的閉包被compare所引用了,而這個閉包函式的Scope Chain又引用了createComparisonFunction的活動物件,因此記憶體增加了這兩個物件。
step3: 執行compare;
這裡寫圖片描述
在執行在執行閉包(compare)的時候,首先會建立閉包的執行時上下文物件 + ScopeChain + 活動物件,然後執行閉包。
閉包執行時記憶體中保留物件:

  1. Global Object
  2. createComparisonFunction的Function物件 + Scope Chain
  3. 閉包Closure(anonymous)的Function物件 + Scope Chain
  4. createComparisonFunction的活動物件
  5. 閉包Closure(anonymous)的執行時上下文 + Scope Chain
  6. 閉包Closure(anonymous)的活動物件

執行完閉包Closure(anonymous)之後,閉包Closure(anonymous)的活動物件會被銷燬,閉包Closure(anonymous)的執行時上下文 + Scope Chain也會被銷燬。
閉包執行完記憶體中保留物件:

  1. Global Object
  2. createComparisonFunction的Function物件 + Scope Chain
  3. 閉包Closure(anonymous)的Function物件 + Scope Chain
  4. createComparisonFunction的活動物件

對比step2,step3執行完畢之後,記憶體中保留的物件沒有增加,這就是正常執行一個函式的情況。
在正常情況下,執行完一個函式之後,記憶體中保留的物件應該與執行前一樣的。
執行閉包因為沒有引入新的引用,所以記憶體中保留的物件保持了一致。

那麼問題來了,createComparisonFunction的活動物件到底怎麼才能被銷燬呢?

我們首先看createComparisonFunction的活動物件存在的原因是閉包Closure(anonymous)的Function物件的Scope Chain引用了createComparisonFunction的活動物件,其目的是因為閉包中需要訪問propertyName這個createComparisonFunction的活動物件中的屬性。

如果閉包Closure(anonymous)的Function物件被銷燬之後,createComparisonFunction的活動物件因為沒有被任何物件引用,也會被銷燬。
step4解除了compare對閉包的引用,就使得閉包沒有被任何物件引用,閉包銷燬,從而使得createComparisonFunction的活動物件因為沒有被任何物件引用,也會被銷燬,這樣就回收了這些物件所佔用的記憶體。
使用閉包的問題就是記憶體消耗會比一般的函式大,因為要儲存額外的活動物件,所以在不需要使用閉包的時候,需要將閉包解引用,回收額外的活動物件所佔用的記憶體。

執行完step4之後記憶體中保留的物件:

  1. Global Object
  2. createComparisonFunction的Function物件 + Scope Chain

對比step1,step4在執行完畢之後,沒有額外的物件被保留在記憶體中,引用閉包所產生的額外物件都得到了回收。