在 變數 中談過變數範圍的問題,當時的說明是:使用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();
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>
> 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>
> 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 };
}
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>
> 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);
}
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>
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;
}
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>
> 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>
js> function func() {
> var x = 20;
> var f = new Function('return x;');
> print(f.__parent__.x);
> return f();
> };
js> func();
10
10
js>