介紹
資料聲明
不同執行內容中的變數對象
全域上下文中的變數對象
函數上下午中的變數對象
分階段處理上下文代碼
進入執行內容
執行代碼
關於變數
特殊實現: __parent__ 屬性
結論
其他參考介紹
我們在建立應用程式的時候,總免不了要聲明變數和函數。那麼,當我們需要使用這些東西的時候,解譯器(interpreter)是怎麼樣、從哪裡找到我們的資料(函數,變數)的,這個過程究竟發生了什麼呢?
大部分ECMAScript程式員應該都知道變數與 執行內容 密切相關:
var a = 10; // variable of the global context (function () { var b = 20; // local variable of the function context})(); alert(a); // 10alert(b); // "b" is not defined
同樣,很多程式員也知道,基於目前的版本的規範,獨立範圍只能通過“函數(function)”代碼類型的執行內容建立。那麼,想對於C/C++舉例來說,ECMAScript裡, for 迴圈並不能建立一個局部的上下文。(譯者註:就是局部範圍):
for (var k in {a: 1, b: 2}) { alert(k);} alert(k); // variable "k" still in scope even the loop is finished
下面我們具體來看一看,當我們聲明資料時候的內部細節。
資料聲明
如果變數與執行內容相關,那麼它自己應該知道它的資料存放區在哪裡和如何訪問。這種機制被稱作 變數對象(variable object).
變數對象 (縮寫為VO)就是與執行內容相關的對象(譯者註:這個“對象”的意思就是指某個東西),它儲存下列內容:
- 變數 (var, VariableDeclaration);
- 函式宣告 (FunctionDeclaration, 縮寫為FD);
- 以及函數的形參
以上均在上下文中聲明。
簡單舉例如下,一個變數對象完全有可能用正常的ECMAScript對象的形式來表現:
VO = {};
正如我們之前所說, VO就是執行內容的屬性(property):
activeExecutionContext = { VO: { // context data (var, FD, function arguments) }};
只有全域上下文的變數對象允許通過VO的屬性名稱間接訪問(因為在全域上下文裡,全域對象自身就是變數對象,稍後會詳細介紹)。在其它上下文中是不可能直接存取到VO的,因為變數對象完全是實現機制內部的事情。
當我們聲明一個變數或一個函數的時候,同時還用變數的名稱和值,在VO裡建立了一個新的屬性。
例如:
var a = 10;function test(x) { var b = 20;};test(30);
對應的變數對象是:
// Variable object of the global contextVO(globalContext) = { a: 10, test: };// Variable object of the "test" function contextVO(test functionContext) = { x: 30, b: 20};
在具體實現層面(和在規範中)變數對象只是一個抽象的事物。(譯者註:這句話翻譯的總感覺不太順溜,歡迎您提供更好的譯文。)從本質上說,在不同的具體執行內容中,VO的名稱和初始結構都不同。
不同執行內容中的變數對象
對於所有類型的執行內容來說,變數對象的一些操作(如變數初始化)和行為都是共通的。從這個角度來看,把變數對象作為抽象的基本事物來理解更容易。而在函數上下文裡同樣可以通過變數對象定義一些相關的額外細節。
下面,我們詳細展開探討;
全域上下文中的變數對象
這裡有必要先給全域對象(Global object)一個明確的定義:
全域對象(Global object) 是在進入任何執行內容之前就已經建立的對象;這個對象只存在一份,它的屬性在程式中任何地方都可以訪問,全域對象的生命週期終止於程式退出那一刻。
初始建立階段,全域對象通過Math,String,Date,parseInt等屬性初始化,同樣也可以附加其它對象作為屬性,其中包括可以引用全域對象自身的對象。例如,在DOM中,全域對象的window屬性就是引用全域對象自身的屬性(當然,並不是所有的具體實現都是這樣):
global = { Math: <...>, String: <...> ... ... window: global};
因為全域對象是不能通過名稱直接存取的,所以當訪問全域對象的屬性時,通常忽略首碼。儘管如此,通過全域內容相關的this還是有可能直接存取到全域對象的,同樣也可以通過引用自身的屬性來訪問,例如,DOM中的window。綜上所述,代碼可以簡寫為:
String(10); // means global.String(10);// with prefixeswindow.a = 10; // === global.window.a = 10 === global.a = 10;this.b = 20; // global.b = 20;
因此,全域上下文中的變數對象就是全域對象自身(global object itself):
VO(globalContext) === global;
準確理解“全域上下文中的變數對象就是全域對象自身”是非常必要的,基於這個事實,在全域上下文中聲明一個變數時,我們才能夠通過全域對象的屬性間接訪問到這個變數(例如,當事先未知變數名時):
var a = new String('test');alert(a); // directly, is found in VO(globalContext): "test"alert(window['a']); // indirectly via global === VO(globalContext): "test"alert(a === this.a); // truevar aKey = 'a';alert(window[aKey]); // indirectly, with dynamic property name: "test"
函數上下文中的變數對象
在函數執行內容中,VO是不能直接存取的,此時由啟用物件(activation object,縮寫為AO)扮演VO的角色。
VO(functionContext) === AO;
啟用物件 是在進入函數上下文時刻被建立的,它通過函數的arguments屬性初始化。grguments屬性的值是Arguments object:
AO = { arguments: <ArgO>};
Arguments objects 是函數上下文裡的啟用物件中的內部對象,它包括下列屬性:
- callee — 指向當前函數的引用;
- length — 真正傳遞的參數的個數;
- properties-indexes (字串類型的整數) 屬性的值就是函數的參數值(按參數列表從左至右排列)。 properties-indexes內部元素的個數等於arguments.length. properties-indexes 的值和實際傳遞進來的參數之間是共用的。(譯者註:共用與不共用的區別可以對比理解為引用傳遞與值傳遞的區別)
例如:
function foo(x, y, z) { alert(arguments.length); // 2 – quantity of passed arguments alert(arguments.callee === foo); // true alert(x === arguments[0]); // true alert(x); // 10 arguments[0] = 20; alert(x); // 20 x = 30; alert(arguments[0]); // 30 // however, for not passed argument z, // related index-property of the arguments // object is not shared z = 40; alert(arguments[2]); // undefined arguments[2] = 50; alert(z); // 40}foo(10, 20);
最後一個例子的情境,在目前的版本的Google Chrome瀏覽器裡有一個bug — 即使沒有傳遞參數z,z和arguments[2]仍然是共用的。(譯者註:我實驗了一下,在Chrome Ver4.1.249.1059版本,該bug仍然存在)
分階段處理上下文代碼
現在我們終於觸及到本文的核心內容。執行內容的代碼被分成兩個基本的階段來處理:
變數對象的變化與這兩個階段緊密相關。
進入執行內容
當進入執行內容(代碼執行之前)時,VO已被下列屬性填充滿(這些都已經在前文描述過):
- 函數的所有形式參數(如果我們是在函數執行內容中)
— 變數對象的一個屬性,這個屬性由一個形式參數的名稱和值組成;如果沒有對應傳遞實際參數,那麼這個屬性就由形式參數的名稱和undefined值組成;
- 所有函式宣告(FunctionDeclaration, FD)
—變數對象的一個屬性,這個屬性由一個函數對象(function-object)的名稱和值組成;如果變數對象已經存在相同名稱的屬性,則完全替換這個屬性。
- 所有變數聲明(var, VariableDeclaration)
—變數對象的一個屬性,這個屬性由變數名稱和undefined值組成;如果變數名稱跟已經聲明的形式參數或函數相同,則變數聲明不會干擾已經存在的這類屬性。
讓我們看一個例子:
function test(a, b) { var c = 10; function d() {} var e = function _e() {}; (function x() {});}test(10); // call
當進入“test”函數的上下文時(傳遞參數10),AO如下:
AO(test) = { a: 10, b: undefined, c: undefined, d: <reference to FunctionDeclaration "d"> e: undefined};
注意,AO裡並不包含函數“x”。這是因為“x” 是一個函數運算式(FunctionExpression, 縮寫為 FE) 而不是函式宣告,函數運算式不會影響VO(譯者註:這裡的VO指的就是AO)。 不管怎樣,函數“_e” 同樣也是函數運算式,但是就像我們下面將看到的那樣,因為它分配給了變數 “e”,所以它變成可以通過名稱“e”來訪問。 FunctionDeclaration 與 FunctionExpression 的不同,將在 Chapter 5. Functions進行詳細的探討。
這之後,將進入處理上下文代碼的第二個階段 — 執行代碼。
執行代碼
這一刻,AO/VO 已經被屬性(不過,並不是所有的屬性都有值,大部分屬性的值還是系統預設的初始值undefined )填滿。
還是前面那個例子, AO/VO 在代碼解釋期間被修改如下:
AO['c'] = 10;AO['e'] = <reference to FunctionExpression "_e">;
再次注意,因為FunctionExpression“_e”儲存到了已聲明的變數“e”上,所以它仍然存在於記憶體中(譯者註:就是還在AO/VO中的意思)。而FunctionExpression。未儲存的函數運算式只有在它自己的定義或遞迴中才能被調用。 “x” 並不存在於AO/VO中。即,如果我們想嘗試調用“x”函數,不管在函數定義之前還是之後,都會出現一個錯誤“x is not defined”
另一個經典例子:
alert(x); // functionvar x = 10;alert(x); // 10x = 20;function x() {};alert(x); // 20
為什麼第一個alert “x” 的傳回值是function,而且它還是在“x” 聲明之前訪問的“x” 的?為什麼不是10或20呢?因為,根據規範 — 當進入上下文時,往VO裡填入函式宣告;在相同的階段,還有一個變數聲明“x”,那麼正如我們在上一個階段所說,變數聲明在順序上跟在函式宣告和形式參數聲明之後,而且,在這個階段(譯者註:這個階段是指進入執行內容階段),變數聲明不會干擾VO中已經存在的同名函式宣告或形式參數聲明,因此,在進入上下文時,VO的結構如下:
VO = {}; VO['x'] = <reference to FunctionDeclaration "x"> // found var x = 10;// if function "x" would not be already defined// then "x" be undefined, but in our case// variable declaration does not disturb// the value of the function with the same name VO['x'] = <the value is not disturbed, still function>
隨後在執行代碼階段,VO做如下修改:
VO['x'] = 10;VO['x'] = 20;
我們可以在第二、三個alert看到這個效果。
在下面的例子裡我們可以再次看到,變數是在進入上下文階段放入VO中的。(因為,雖然else部分代碼永遠不會執行,但是不管怎樣,變數“b”仍然存在於VO中。)(譯者註:變數b雖然存在於VO中,但是變數b的值永遠是undefined)
if (true) { var a = 1;} else { var b = 2;}alert(a); // 1alert(b); // undefined, but not "b is not defined"
關於變數
通常,各類文章和JavaScript相關的書籍都聲稱:“不管是使用var關鍵字(在全域上下文)還是不使用var關鍵字(在任何地方),都可以聲明一個變數”。請記住,這絕對是謠傳:
任何時候,變數只能通過使用var關鍵字才能聲明。
那麼像下面這樣分配:
a = 10;
這僅是給全域對象建立了一個新屬性(但是它不是變數)。“不是變數”的意思並不是說它不能被改變,而是指它不符合ECMAScript規範中的變數概念,所以它“不是變數”(它之所以能成為全域對象的屬性,完全是因為VO(globalContext) === global,大家還記得這個吧?)。
讓我們通過下面的執行個體看看具體的區別吧:
alert(a); // undefinedalert(b); // "b" is not definedb = 10;var a = 20;
所有根源仍然是VO和它的修改階段(進入上下文 階段和執行代碼 階段):
進入上下文階段:
VO = { a: undefined};
我們可以看到,因為“b”不是一個變數,所以在這個階段根本就沒有“b”,“b”將只在執行代碼階段才會出現(但是在我們這個例子裡,還沒有到那就已經出錯了)。
讓我們改變一下例子代碼:
alert(a); // undefined, we know whyb = 10;alert(b); // 10, created at code executionvar a = 20;alert(a); // 20, modified at code execution
關於變數,還有一個重要的知識點。變數相對於簡單屬性來說,變數有一個特性(attribute):{DontDelete},這個特性的含義就是不同通過delete操作符直接刪除變數屬性。
a = 10;alert(window.a); // 10alert(delete a); // truealert(window.a); // undefinedvar b = 20;alert(window.b); // 20alert(delete b); // falsealert(window.b); // still 20
但是,在eval上下文,這個規則並不起作用,因為在這個上下文裡,變數沒有{DontDelete}特性。
eval('var a = 10;');alert(window.a); // 10alert(delete a); // truealert(window.a); // undefined
使用一些調試工具(例如:Firebug)的控制台測試該執行個體時,請注意,Firebug同樣是使用eval來執行控制台裡你的代碼。因此,變數屬性同樣沒有{DontDelete}特性,可以被刪除。
特殊實現: __parent__ 屬性
前面已經提到過,按標準規範,啟用物件是不可能被直接存取到的。但是,一些具體實現並沒有完全遵守這個規定,例如SpiderMonkey和Rhino;在這些具體實現中,函數有一個特殊的屬性 __parent__,通過這個屬性可以直接引用到函數已經建立的啟用物件或全域變數對象。
例如 (SpiderMonkey, Rhino):
var global = this;var a = 10;function foo() {}alert(foo.__parent__); // globalvar VO = foo.__parent__;alert(VO.a); // 10alert(VO === global); // true
在上面的例子中我們可以看到,函數foo是在全域上下文中建立的,所以屬性__parent__ 指向全域內容相關的變數對象,即全域對象。(譯者註:還記得這個吧:VO(globalContext) === global)
然而,在SpiderMonkey中用同樣的方式訪問啟用物件是不可能的:在不同版本的SpiderMonkey中,內建函式的__parent__ 有時指向null ,有時指向全域對象。
在Rhino中,用同樣的方式訪問啟用物件是完全可以的。
例如 (Rhino):
var global = this;var x = 10;(function foo() { var y = 20; // the activation object of the "foo" context var AO = (function () {}).__parent__; print(AO.y); // 20 // __parent__ of the current activation // object is already the global object, // i.e. the special chain of variable objects is formed, // so-called, a scope chain print(AO.__parent__ === global); // true print(AO.__parent__.x); // 10})();
結論
在這篇文章裡,我們進一步深入學習了跟執行內容相關的對象。我希望這些知識對您來說能有所協助,能解決一些您曾經遇到的問題或困惑。按照計劃,在後續的章節中,我們將探討Scope chain, Identifier resolution ,Closures。
如果您有問題,我很高興在下面評論中解答。
其他參考
- 10.1.3 – Variable Instantiation;
- 10.1.5 – Global Object;
- 10.1.6 – Activation Object;
- 10.1.8 – Arguments Object.
英文地址 : ECMA-262-3 in detail.Chapter 2.Variable object
中文地址 : [JavaScript]ECMA-262-3 深入解析.第二章.變數對象