JavaScript ECMA-262-3 深入解析(二):變數物件例項詳解
本文例項講述了JavaScript ECMA-262-3變數物件。分享給大家供大家參考,具體如下:
介紹
我們在建立應用程式的時候,總免不了要宣告變數和函式。那麼,當我們需要使用這些東西的時候,直譯器(interpreter)是怎麼樣、從哪裡找到我們的資料(函式,變數)的,這個過程究竟發生了什麼呢?
大部分ECMAScript程式設計師應該都知道變數與 執行上下文 密切相關:
var a = 10; // variable of the global context (function () { var b = 20; // local variable of the function context })(); alert(a); // 10 alert(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 context VO(globalContext) = { a: 10,test: }; // Variable object of the "test" function context VO(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 prefixes window.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); // true var 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); // function var x = 10; alert(x); // 10 x = 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); // 1 alert(b); // undefined,but not "b is not defined"
關於變數
通常,各類文章和JavaScript相關的書籍都聲稱:“不管是使用var關鍵字(在全域性上下文)還是不使用var關鍵字(在任何地方),都可以宣告一個變數”。請記住,這絕對是謠傳:
任何時候,變數只能通過使用var關鍵字才能宣告。
那麼像下面這樣分配:
a = 10;
這僅是給全域性物件建立了一個新屬性(但是它不是變數)。“不是變數”的意思並不是說它不能被改變,而是指它不符合ECMAScript規範中的變數概念,所以它“不是變數”(它之所以能成為全域性物件的屬性,完全是因為VO(globalContext) === global,大家還記得這個吧?)。
讓我們通過下面的例項看看具體的區別吧:
alert(a); // undefined alert(b); // "b" is not defined b = 10; var a = 20;
所有根源仍然是VO和它的修改階段(進入上下文 階段和執行程式碼 階段):
進入上下文階段:
VO = { a: undefined };
我們可以看到,因為“b”不是一個變數,所以在這個階段根本就沒有“b”,“b”將只在執行程式碼階段才會出現(但是在我們這個例子裡,還沒有到那就已經出錯了)。
讓我們改變一下例子程式碼:
alert(a); // undefined,we know why b = 10; alert(b); // 10,created at code execution var a = 20; alert(a); // 20,modified at code execution
關於變數,還有一個重要的知識點。變數相對於簡單屬性來說,變數有一個特性(attribute):{DontDelete},這個特性的含義就是不同通過delete操作符直接刪除變數屬性。
a = 10; alert(window.a); // 10 alert(delete a); // true alert(window.a); // undefined var b = 20; alert(window.b); // 20 alert(delete b); // false alert(window.b); // still 20
但是,在eval上下文,這個規則並不起作用,因為在這個上下文裡,變數沒有{DontDelete}特性。
eval('var a = 10;'); alert(window.a); // 10 alert(delete a); // true alert(window.a); // undefined
使用一些除錯工具(例如:Firebug)的控制檯測試該例項時,請注意,Firebug同樣是使用eval來執行控制檯裡你的程式碼。因此,變數屬性同樣沒有{DontDelete}特性,可以被刪除。
特殊實現: __parent__ 屬性
前面已經提到過,按標準規範,啟用物件是不可能被直接訪問到的。但是,一些具體實現並沒有完全遵守這個規定,例如SpiderMonkey和Rhino;在這些具體實現中,函式有一個特殊的屬性 __parent__,通過這個屬性可以直接引用到函式已經建立的啟用物件或全域性變數物件。
例如 (SpiderMonkey,Rhino):
var global = this; var a = 10; function foo() {} alert(foo.__parent__); // global var VO = foo.__parent__; alert(VO.a); // 10 alert(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。
如果您有問題,我很高興在下面評論中解答。
英文地址 : ECMA-262-3 in detail.Chapter 2.Variable object
感興趣的朋友可以使用線上HTML/CSS/JavaScript程式碼執行工具:http://tools.jb51.net/code/HtmlJsRun測試上述程式碼執行效果。
更多關於JavaScript相關內容可檢視本站專題:《javascript面向物件入門教程》、《JavaScript錯誤與除錯技巧總結》、《JavaScript資料結構與演算法技巧總結》、《JavaScript遍歷演算法與技巧總結》及《JavaScript數學運算用法總結》
希望本文所述對大家JavaScript程式設計有所幫助。