在〈使用 var 宣告變數 〉中談過變數範圍,當時說過,使用 var
宣告的變數,作用範圍基本上是在函式的範圍之中,若是在全域範圍,會是全域物件上的一個特性,也就是俗稱的全域範圍。
在〈Closure 與一級函式〉中則以置閒變數觀念,從語法層面說明 Closure 的意義與作用,也可應付絕大多數的情況。
事實上,JavaScript 在查找變數時,會循著 Scope chain 逐一查找,Scope chain 可說明為何 JavaScript 沒有區塊範圍,也是 JavaScript 中閉包的實現機制。
就結論而言,你可沿著 Scope chain 來查找變數,也就是看看函式自身的 context 物件上是否有該特性,如果沒有就往外頭的 context 物件看看有沒有該特性。
如果要稍微深入一下 JavaScript 的 Scope chain,首先得了解一些名詞。首先是 Lexical Scope,這代表著 JavaScript 原始碼的物理位置(Physical placement)。例如:
var x = 10;
function outer() {
var y = 20;
function inner() {
var z = 30;
}
}
outer();
在結構上,函式 inner
在原始碼中的位置是被 outer
包裹,而 outer
則是被 Global context 包裹,這樣的結構在原始碼寫下後就不變了,是屬於靜態的結構部份。每段 JavaScript 程式碼都會執行在一個 Execution context 中,對應 Lexical Scope 就是 Execution context。
像是上例中,x
變數定義是在 Global execution context 中,而每個函式在呼叫時都會建立新的 Function execution context,有個物件用來代表 Execution context,而區域變數則是 context 物件上的特性。
查找變數就是依序在 context 物件上尋找特性,這可用來說明以下這個例子:
> function func() {
... console.log(m);
... var m = 10;
... console.log(m);
... }
undefined
> func();
undefined
10
undefined
>
逐步來看的話,就是這樣:
function func() {
// 由於 Hoisting 的關係,func 的 context 物件上有 m 特性
// 但還沒指定值,因此是 undefined
console.log(m);
var m = 10; // func 的 context 物件有 m 特性且值為 10
console.log(m); // func 的 context 物件有 m 特性且值為 10
}
JavaScript 在查找變數時,會先在目前 context 物件上找,若找不到指定名稱,則會到外層 context 物件上找,若找不到,就再到更外層 context 物件找,直到全域物件為止,這樣的順序形成變數查找的 Scope chain。
再來看區域變數覆蓋全域變數的例子:
var x = 10;
function func() {
var x = 20;
console.log(x);
}
以 Scope chain 來解釋的話,在 func
函式中查找 x
時,是先查找 func
的 context 物件上有無 x
特性,因此對應的是 20 的值。
再來看 Closure 的例子:
function doSome() {
var x = 10;
function f(y) {
return x + y;
}
return f;
}
在內部的 f
函式中 context 物件上找有無 x
特性時,並沒有找到,於是在包裹 f
的 doSome
呼叫物件上查找有無 x
,此時找到了。
可以這麼說,在 JavaScript 中,所有的變數,都是某個物件上的特性。
附帶一提的是,自行使用 new
建立的 Function
,如果有查找變數,一定查找全域物件中的特性。例如:
> var x = 10;
undefined
> function func() {
... var x = 20;
... var f = new Function('return x;');
... return f();
... }
undefined
> func();
10
>