範圍鏈(Scope chain)


變數 中談過變數範圍的問題,當時的說明是:使用var所宣告的變數,作用範圍是在當時所在環境,不使用var直接指定值而建立的變數,則是全域物件上的一個特性,也就是俗稱的全域範圍。單就使用JavaScript而言,這就足以了解使用變數時,有無使用var的差別。

閉包(Closure) 中則以置閒變數的觀念,從語法層面說明閉包的意義與作用,也可應付絕大多數的情況。

事實上,JavaScript在查找變數時,會循著範圍鏈(Scope chain)一層一層往外找,範圍鏈的觀念可說明為何JavaScript沒有區塊範圍,也可是JavaScript中閉包的實現機制。

要了解範圍鏈,首先得了解一些名詞。首先是語彙範圍(Lexical Scope),這代表著JavaScript原始碼的物理位置(Physical placement)。例如:
var x = 10;
function outer() {
    var y = 20;
    function inner() {
        var z = 30;
    }
}
func();

在結構上,函式inner()在原始碼中的位置是被outer()包裹,而outer()則是被全域環境(global context)包裹,這樣的結構在原始碼寫下後就不變了,是屬於靜態的結構部份。

每段JavaScript程式碼都會執行在一個執行環境(Execution context)中,像是上例中,x變數的指定是在全域執行環境(Global execution context)中,而每個函式在呼叫時,會建立新的函式執行環境(function execution context),接著建立活化物件(Activation object,又稱呼叫物件(Call object),接著在呼叫物件上建立arguments特性。

每個函式都會有個內部的[[scope]]特性(無法直接存取)
執行環境遇到函式時,會將[[scope]]參考至一個範圍鏈(Scope chain),而呼叫物件放在範圍鏈第一個位置,接著就開始進行處理函式中的變數,每個函式中的變數,都會成為呼叫物件上的特性(此時的呼叫物件,又稱之為變數物件[Variable object])。

函式的[[scope]]無法直接存取,但在
Rhino直譯器中,可以使用函式上非標準的__parent__取得語彙範圍(Lexical Scope)建立時,包裹該函式的外部函式之呼叫物件。以上例來說,outer.__parent__是包裹outer函式的全域環境中的呼叫物件,對頂層函式而言也就是全域物件(當時範圍鏈中也只有這麼一個物件),而inner.__parent__可取得outer的呼叫物件,如果要取得範圍鏈下一個物件,則可以使用inner.__parent__.__parent__,此時取得的就是全域物件

單看這個過程著實有點複雜,這是直譯器實作的一些細節,結論就是,你可以沿著範圍鏈一路往上查找變數,而這就是
JavaScript查找變數的基本作法

例如:

js> var x = 10;
  > function outer() {
  >     var y = 20;
  >     function inner() {
  >         var z = 30;
  >     }
  >     return inner;
  > }
js> var f = outer();
js> outer.__parent__ == this;
true
js> f.__parent__.__parent__ == this;
true
js> f.__parent__.y;
20
js> f.__parent__.__parent__.x;
10
js>


所謂的變數,其實就是呼叫物件(活化物件、變數物件都是同意詞)上的特性。以上例而言,你可以透過inner.__parent__參考至outer的呼叫物件,而outer中宣告的y,其實就是呼叫物件上的特性,而f.__parent__.__parent__取得範圍鏈下一個物件,也就是全域物件。

這可以說明這個例子:

js> function func() {
  >     print(m);
  >     var m = 10;
  >     print(m);
  > }
js> func();
undefined
10
js>


逐步來看的話,就是這樣:
function func() {
    print(m);         // variableObject = { m : undefined };
    var m = 10;       // variableObject = { m : 10 };
    print(m);         // variableObject = { m : 10 };
}

所以,若使用非標準的__parent__,你也可以這麼取得相同結果:

js> function func() {
  >     function inner() {}
  >     print(inner.__parent__.m);
  >     var m = 10;
  >     print(inner.__parent__.m);
  > }
js> func();
undefined
10
js>

JavaScript在查找變數時,會先在目前範圍鏈第一個物件上找,若找不到指定名稱,則會到範圍鏈下一個物件上找,若找不到,就再到範圍鏈下一個物件找,直到全域物件為止。

再來看區域變數覆蓋全域變數的例子:
var x = 10;
function func() {
    var x = 20;
    print(x);
}

以範圍鏈來解釋的話,在func()函式中查找x時,是先查找func()的呼叫物件上有無x特性,因此對應的是20的值:

js> var x = 10;
js> function func() {
  >     function inner() {}
  >     var x = 20;
  >     print(inner.__parent__.x);
  > }
js> func();
20
js>

再來看閉包的例子:
function doSome() {
    var x = 10;
    function f(y) {
        return x + y;
    }
    return f;
}

在內部的f中呼叫物件上找有無x特性時,並沒有找到,於是在包裹f的doSome呼叫物件上查找有無x,也就是查找f.__parent__上有無x,此時找到了。

如果你了解範圍鏈,結合非標準__parent__,你也可以這麼玩:

js> function func() {
  >     function inner() {}
  >     inner.__parent__.y = 30;
  >     print(y);
  > }
js> func();
30
js>

即便func()中並沒有使用var宣告y,但仍可以顯示y的值。

所以可以這麼說,在
JavaScript中,所有的變數,都是某個物件上的特性。

附帶一提的是,自行使用new建立的Function,如果有查找變數,一定查找全域物件中的特性。例如:
js> var x = 10;
js> function func() {
  >     var x = 20;
  >     var f = new Function('return x;');
  >     print(f.__parent__.x);
  >     return f();
  > };
js> func();
10
10
js>