前言
在第12章關於變數對象的描述中,我們已經知道一個執行內容 的資料(變數、函式宣告和函數的形參)作為屬性儲存區在變數對象中。
同時我們也知道變數對象在每次進入上下文時建立,並填入初始值,值的更新出現在代碼執行階段。
這一章專門討論與執行內容直接相關的更多細節,這次我們將提及一個議題——範圍鏈。
英文原文:http://dmitrysoshnikov.com/ecmascript/chapter-4-scope-chain/
中文參考:http://www.denisdeng.com/?p=908
本文絕大部分內容來自上述地址,僅做少許修改,感謝作者
定義
如果要簡要的描述並展示其重點,那麼範圍鏈大多數與內建函式相關。
我們知道,ECMAScript 允許建立內建函式,我們甚至能從父函數中返回這些函數。
var x = 10;
function foo() {
var y = 20;
function bar() {
alert(x + y);
}
return bar;
}
foo()(); // 30
這樣,很明顯每個上下文擁有自己的變數對象:對於全域上下文,它是全域對象自身;對於函數,它是使用中的物件。
範圍鏈正是內部內容所有變數對象(包括父變數對象)的列表。此鏈用來變數查詢。即在上面的例子中,“bar”內容相關的範圍鏈包括AO(bar)、AO(foo)和VO(global)。
但是,讓我們仔細研究這個問題。
讓我們從定義開始,並進深一步的討論樣本。
範圍鏈與一個執行內容相關,變數對象的鏈用於在標識符解析中變數尋找。
函數內容相關的範圍鏈在函數調用時建立的,包含使用中的物件和這個函數內部的[[scope]]屬性。下面我們將更詳細的討論一個函數的[[scope]]屬性。
在上下文中示意如下:
activeExecutionContext = {
VO: {...}, // or AO
this: thisValue,
Scope: [ // Scope chain
// 所有變數對象的列表
// for identifiers lookup
]
};
其scope定義如下:
Scope = AO + [[Scope]]
這種聯合和標識符解析過程,我們將在下面討論,這與函數的生命週期相關。
函數的生命週期
函數的的生命週期分為建立和啟用階段(調用時),讓我們詳細研究它。
函數建立
眾所周知,在進入上下文時函式宣告放到變數/活動(VO/AO)對象中。讓我們看看在全域上下文中的變數和函式宣告(這裡變數對象是全域對象自身,我們還記得,是吧?)
var x = 10;
function foo() {
var y = 20;
alert(x + y);
}
foo(); // 30
在函數啟用時,我們得到正確的(預期的)結果--30。但是,有一個很重要的特點。
此前,我們僅僅談到有關當前內容相關的變數對象。 這裡,我們看到變數“y”在函數“foo”中定義(意味著它在foo內容相關的AO中),但是變數“x”並未在“foo”上下文中定義,相應地,它也不會添 加到“foo”的AO中。乍一看,變數“x”相對於函數“foo”根本就不存在;但正如我們在下面看到的——也僅僅是“一瞥”,我們發現,“foo”上下 文的使用中的物件中僅包含一個屬性--“y”。
fooContext.AO = {
y: undefined // undefined – 進入內容相關的時候是20 – at activation
};
函數“foo”如何訪問到變數“x”?理論上函數應該能訪問一個更高一層內容相關的變數對象。實際上它正是這樣,這種機制是通過函數內部的[[scope]]屬性來實現的。
[[scope]]是所有父變數對象的層級鏈,處於當前函數上下文之上,在函數建立時存於其中。
注意這重要的一點--[[scope]]在函數建立時被儲存--靜態(不變的),永遠永遠,直至函數銷毀。即:函數可以永不調用,但[[scope]]屬性已經寫入,並儲存在函數對象中。
另外一個需要考慮的是--與範圍鏈對比,[[scope]]是函數的一個屬性而不是上下文。考慮到上面的例子,函數“foo”的[[scope]]如下:
foo.[[Scope]] = [
globalContext.VO // === Global
];
舉例來說,我們用通常的ECMAScript 數組展現範圍和[[scope]]。
繼續,我們知道在函數調用時進入上下文,這時候使用中的物件被建立,this和範圍(範圍鏈)被確定。讓我們詳細考慮這一時刻。
函數啟用
正如在定義中說到的,進入上下文建立AO/VO之後,內容相關的Scope屬性(變數尋找的一個範圍鏈)作如下定義:
Scope = AO|VO + [[Scope]]
上面代碼的意思是:使用中的物件是範圍數組的第一個對象,即添加到範圍的前端。
Scope = [AO].concat([[Scope]]);
這個特點對於標示符解析的處理來說很重要。
標示符解析是一個處理過程,用來確定一個變數(或函式宣告)屬於哪個變數對象。
這個演算法的傳回值中,我們總有一個參考型別,它的base組件是相應的變數對象(或若未找到則為null),屬性名稱組件是向上尋找的標示符的名稱。參考型別的詳細資料在第13章.this中已討論。
標識符解析過程包含與變數名對應屬性的尋找,即範圍中變數對象的連續尋找,從最深的上下文開始,繞過範圍鏈直到最上層。
這樣一來,在向上尋找中,一個上下文中的局部變數較之於父範圍的變數擁有較高的優先順序。萬一兩個變數有相同的名稱但來自不同的範圍,那麼第一個被發現的是在最深範圍中。
我們用一個稍微複雜的例子描述上面講到的這些。
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
alert(x + y + z);
}
bar();
}
foo(); // 60
對此,我們有如下的變數/使用中的物件,函數的的[[scope]]屬性以及內容相關的範圍鏈:
全域內容相關的變數對象是:
globalContext.VO === Global = {
x: 10
foo: <reference to function>
};
在“foo”建立時,“foo”的[[scope]]屬性是:
foo.[[Scope]] = [
globalContext.VO
];
在“foo”啟用時(進入上下文),“foo”內容相關的使用中的物件是:
fooContext.AO = {
y: 20,
bar: <reference to function>
};
“foo”內容相關的範圍鏈為:
fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
fooContext.Scope = [
fooContext.AO,
globalContext.VO
];
內建函式“bar”建立時,其[[scope]]為:
bar.[[Scope]] = [
fooContext.AO,
globalContext.VO
];
在“bar”啟用時,“bar”內容相關的使用中的物件為:
barContext.AO = {
z: 30
};
“bar”內容相關的範圍鏈為:
barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:
barContext.Scope = [
barContext.AO,
fooContext.AO,
globalContext.VO
];
對“x”、“y”、“z”的標識符解析如下:
- "x"
-- barContext.AO // not found
-- fooContext.AO // not found
-- globalContext.VO // found - 10
- "y"
-- barContext.AO // not found
-- fooContext.AO // found - 20
- "z"
-- barContext.AO // found - 30
範圍特徵
讓我們看看與範圍鏈和函數[[scope]]屬性相關的一些重要特徵。
閉包
在ECMAScript中,閉包與函數的[[scope]]直接相關,正如我們提到的那樣,[[scope]]在函數建立時被儲存,與函數共存亡。 實際上,閉包是函數代碼和其[[scope]]的結合。因此,作為其對象之一,[[Scope]]包括在函數內建立的詞法範圍(父變數對象)。當函數進 一步啟用時,在變數對象的這個詞法鏈(靜態儲存於建立時)中,來自較高範圍的變數將被搜尋。
例如:
var x = 10;
function foo() {
alert(x);
}
(function () {
var x = 20;
foo(); // 10, but not 20
})();
我們再次看到,在標識符解析過程中,使用函數建立時定義的詞法範圍--變數解析為10,而不是30。此外,這個例子也清晰的表明,一個函數(這個例子中為從函數“foo”返回的匿名函數)的[[scope]]持續存在,即使是在函數建立的範圍已經完成之後。
關於ECMAScript中閉包的理論和其執行機制的更多細節,閱讀16章閉包。
通過建構函式建立的函數的[[scope]]
在上面的例子中,我們看到,在函數建立時獲得函數的[[scope]]屬性,通過該屬性訪問到所有父內容相關的變數。但是,這個規則有一個重要的例外,它涉及到通過函數建構函式建立的函數。
var x = 10;
function foo() {
var y = 20;
function barFD() { // 函式宣告
alert(x);
alert(y);
}
var barFE = function () { // 函數運算式
alert(x);
alert(y);
};
var barFn = Function('alert(x); alert(y);');
barFD(); // 10, 20
barFE(); // 10, 20
barFn(); // 10, "y" is not defined
}
foo();
我們看到,通過函數建構函式(Function constructor)建立的函數“bar”,是不能訪問變數“y”的。但這並不意味著函數“barFn”沒有[[scope]]屬性(否則它不能訪問 到變數“x”)。問題在於通過函建構函式建立的函數的[[scope]]屬性總是唯一的全域對象。考慮到這一點,如通過這種函數建立除全域之外的最上層的 上下文閉包是不可能的。
二維範圍鏈尋找
在範圍鏈中尋找最重要的一點是變數對象的屬性(如果有的話)須考慮其中--源於ECMAScript 的原型特性。如果一個屬性在對象中沒有直接找到,查詢將在原型鏈中繼續。即常說的二維鏈尋找。(1)範圍鏈環節;(2)每個範圍鏈--深入到原型鏈環 節。如果在Object.prototype 中定義了屬性,我們能看到這種效果。
function foo() {
alert(x);
}
Object.prototype.x = 10;
foo(); // 10
使用中的物件沒有原型,我們可以在下面的例子中看到:
function foo() {
var x = 20;
function bar() {
alert(x);
}
bar();
}
Object.prototype.x = 10;
foo(); // 20
如果函數“bar”內容相關的啟用物件有一個原型,那麼“x”將在Object.prototype 中被解析,因為它在AO中不被直接解析。但在上面的第一個例子中,在標識符解析中,我們到達全域對象(在一些執行中並不全是這樣),它從 Object.prototype繼承而來,響應地,“x”解析為10。
同樣的情況出現在一些版本的SpiderMokey 的命名函數運算式(縮寫為NFE)中,在那裡特定的Object Storage Service從Object.prototype繼承而來的函數運算式的可選名稱,在Blackberry 中的一些版本中,執行時啟用物件從Object.prototype繼承。但是,關於該特色的更多細節在第15章函數討論。
全域和eval上下文中的範圍鏈
這裡不一定很有趣,但必須要提示一下。全域內容相關的範圍鏈僅包含全域對象。代碼eval的上下文與當前的調用上下文(calling context)擁有同樣的範圍鏈。
globalContext.Scope = [
Global
];
evalContext.Scope === callingContext.Scope;
代碼執行時對範圍鏈的影響
在ECMAScript 中,在代碼執行階段有兩個聲明能修改範圍鏈。這就是with聲明和catch語句。它們添加到範圍鏈的最前端,對象須在這些聲明中出現的標識符中尋找。如果發生其中的一個,範圍鏈簡要的作如下修改:
Scope = withObject|catchObject + AO|VO + [[Scope]]
在這個例子中添加對象,對象是它的參數(這樣,沒有首碼,這個對象的屬性變得可以訪問)。
var foo = {x: 10, y: 20};
with (foo) {
alert(x); // 10
alert(y); // 20
}
範圍鏈修改成這樣:
Scope = foo + AO|VO + [[Scope]]
我們再次看到,通過with語句,對象中標識符的解析添加到範圍鏈的最前端:
var x = 10, y = 10;
with ({x: 20}) {
var x = 30, y = 30;
alert(x); // 30
alert(y); // 30
}
alert(x); // 10
alert(y); // 30
在進入上下文時發生了什嗎?標識符“x”和“y”已被添加到變數對象中。此外,在代碼運行階段作如下修改:
- x = 10, y = 10;
- 對象{x:20}添加到範圍的前端;
- 在with內部,遇到了var聲明,當然什麼也沒建立,因為在進入上下文時,所有變數已被解析添加;
- 在第二步中,僅修改變數“x”,實際上對象中的“x”現在被解析,並添加到範圍鏈的最前端,“x”為20,變為30;
- 同樣也有變數對象“y”的修改,被解析後其值也相應的由10變為30;
- 此外,在with聲明完成後,它的特定對象從範圍鏈中移除(已改變的變數“x”--30也從那個對象中移除),即範圍鏈的結構恢複到with得到加強以前的狀態。
- 在最後兩個alert中,當前變數對象的“x”保持同一,“y”的值現在等於30,在with聲明運行中已發生改變。
同樣,catch語句的異常參數變得可以訪問,它建立了只有一個屬性的新對象--異常參數名。圖示看起來像這樣:
try {
...
} catch (ex) {
alert(ex);
}
範圍鏈修改為:
var catchObject = {
ex: <exception object>
};
Scope = catchObject + AO|VO + [[Scope]]
在catch陳述式完成運行之後,範圍鏈恢複到以前的狀態。
結論
在這個階段,我們幾乎考慮了與執行內容相關的所有常用概念,以及與它們相關的細節。按照計劃--函數對象的詳細分析:函數類型(函式宣告,函數表 達式)和閉包。順便說一下,在這篇文章中,閉包直接與[[scope]]屬性相關,但是,關於它將在合適的篇章中討論。我很樂意在評論中回答你的問題。
其它參考
- 8.6.2 – [[Scope]]
- 10.1.4 – Scope Chain and Identifier Resolution
轉自:湯姆大叔