一般在編程的時候,我們會定義函數和變數來成功的構造我們的系統。但是解析器該如何找到這些資料(函數,變數)呢?當我們引用需要的對象時,又發生了什麼了?
很多ECMAScript編程人員都知道變數和所處的執行內容環境是密切相關的:
var a=10;//全域上下文環境下的變數
(function(){
var b=20;//函數上下文環境下的局部變數
})();
alert(a);//10
alert(b);//"b" 未定義
當然,許多編程人員也知道。在當前規範版本下,隔離的範圍只能由“function”代碼的執行內容產生。與c/c++不同的是,例如ECMAScript中的for迴圈語句塊不能產生局部的執行內容:
for(var k in {a:1,b:2}){
alert(k);
}
alert(k);//即使迴圈結束,變數'k'任然在範圍中
下面讓我們看看,當我們聲明我們的資料時發生的更多的細節。
資料聲明
如果變數和執行內容是密切聯絡的,就應該知道資料存放區在哪裡,如何擷取這些資料。這種機制就稱為變數對象。
變數對象(VO)是一個與執行內容和其儲存位置密切聯絡的特殊對象:
變數(var ,變數聲明);
函式宣告(FD);
函數形參;
在上下文中被聲明。注意,在EC5中用詞法環境模式取代了變數對象。
理論上,可以把變數對象表示為一個常規的ECMAScript對象:VO={};正如我們所說,VO是執行內容的一個屬性:
activeExecutionContext={
Vo:{
//上下文資料(var,FD,function arguments)
}
};
一般不能直接引用變數。僅僅能(通過VO的屬性名稱)引用全域內容相關的變數對象(全域對象就是他自身的變數對象)。至於其他的執行內容直接引用VO是不可能的,它僅僅是一種實現層面的純粹機制。
當我們聲明一個變數或者函數時。我們除了構造VO的包含變數名稱和變數值的屬性,再沒有其他東西了。比如:
var a=10;
function test(x){
var b=20;};
test(30);
相應的變數對象是:
//全域環境下的變數對象
VO(globalContext)={
a=10,
test:<reerence to function>
};
//"test"函數內容相關的變數對象
VO(test functionContext)={
x:30,
b:20
};
但是在執行階段(標準下),變數對象是一個抽象的本質。在具體的執行內容中,VO的命名方式不同且有不同的初始結構。
不同執行內容中的變數對象
變數對象的一些操作(比如變數賦值)和行為在所有的執行內容類型中都是相同的。從這一個角度看,把變數對象表示為一個抽象的基本概念是很方便的。函數上下文也可以定義一些與變數對象相關的附加資訊。
AbstratVO(變數對象執行個體化的一般過程)
║
╠══> GlobalContextVO
║ (VO === this === global)
║
╚══> FunctionContextVO
(VO === AO, <arguments> object and <formal parameters> are added)
下面讓我們詳細的來討論下。
全域上下文變數對象
在這裡,首先應該給出全域對象的定義:全域對象是在進入任何執行內容前就已經構造出的一個對象;全域對象是唯一的(譯者註:單例模式),在程式中的任何地方都可以擷取它的屬性,其生命週期隨著程式的結束而結束。
構造的全域對象被諸如Math,String,Date,parseInt等屬性初始化。也可以通過一些可以引用全域對象自身的附加對象初始化。例如,在BOM中,全域對象的的window屬性指向全域對象(然而,不是所有的實現都是這樣的)
global={
Math:<...>,
String:<...>,
....
....
window:global
};
當引用全域對象屬性時,首碼通常是被省略的,因為全域對象不能直接通過名稱擷取。可能要通過全域上下文中的this值來擷取,也可以通過遞迴引用它自身擷取,例如BOM中的window,可以簡寫為:
String(10);//表示global.String(10) ;
//有首碼
window.a=10;//===global.window.a=10===global.a=10;
this.b=20;//global.b=20
因此,回到全域上下文中的變數對象—這裡的變數對象就是全域對象自身:VO(globalContex)===global;
鑒於這些原因必須準確理解這個事實:在全域上下文聲明的一個變數,我們可以通過全域對象的屬性間接引用它(例如變數名是未知的)
var a=new String('test')
alert(a);//直接引用,在VO(globalCOntext):"test"
alert(window['a']);//間接引用===VO(globalContext):"test"
alert(a===this.a);//true
var akey='a';
alert(window[akey]);//間接引用,通過動態屬性名稱:"test"
函數內容相關的變數對象
對於函數執行內容—VO是不能直接擷取的,它的角色由使用中的物件(AO)扮演。VO(functionContext)===AO;當進入到一個函數上下文時,就產生了使用中的物件。並由值為Arguments對象的arguments屬性初始化。
1 AO={arguments:<Arguments Object> }
Arguments對象是使用中的物件的屬性。它包含了以下屬性:
callee--函數自身的引用;
length--實參個數;
properties- indexes(整數,轉化為字元),其值是函數參數的值(參數列表從左至右)。properties- indexes==arguments.length.也就是參數對象的properties-indexes值和當前(實際傳入值)的形參是共用的
function foo(x, y, z) {
// 已定義的函數參數 (x, y, z)個數
alert(foo.length); // 3
// 實際傳參數量(only x, y)
alert(arguments.length); // 2
// 函數自身的引用
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
// 然而對於未傳參的z,arguments參數對象的索引屬性時不共用的
z = 40;
alert(arguments[2]); // undefined
arguments[2] = 50;
alert(z); // 40
}
在低版本的google瀏覽器中參數共用存在漏洞。在EC5中。使用中的物件的概念已經被詞法環境的公有和單例模式所取代。
處理上下文代碼的階段
現在我們進入到文章的重點,處理執行內容代碼分為兩個階段:
進入執行內容;
執行代碼。
變數對象的修正與這兩個階段也是密切相關的。需要注意的是,這兩個階段的處理過程是一般性的行為並獨立於上下文類型(也就是說,這個過程對於兩種執行內容-函數和全域都是平等的)
進入執行內容
在進入執行內容時(在代碼執行執行前),VO已經被以下屬性(他們已經在前文中提到)填充。
對於函數的每一個形參(如果我們已經進入了函數執行內容)--- 一個含有名稱和形參值的變數對象屬性就建立了,參數還未傳值--也就是含有形參名和其值為undefined的屬性被建立。
對於每一個函式宣告(FD)--- 一個含有函數對象名稱和值的屬性就建立了;如果變數對象已經包含了同名的屬性,覆蓋他的值和特性;
對於每一個變數聲明--- 一個含有變數名和其值為undefined的屬性就建立了;如果這個變數名和已經聲明的形參或函數名稱一樣,變數聲明不能與已經存在的屬性衝突(譯者註:此變數名稱不可用,換之)。
讓我們看下面的例子;
function test(a,b){
var c=10;
function d(){};
var e=function _e(){};
(function x(){});
}
test(10)
當進入含有實參10的test函數上下文時,AO如下:
AO(test) = {
a: 10,
b: undefined,
c: undefined,
d: <reference to FunctionDeclaration "d">
e: undefinedhttp://i.cnblogs.com/EditPosts.aspx?postid=3711963
};
注意,這個AO不包含函數X,這是因為X不是一個函式宣告而是函數運算式(FE),運算式不影響VO。然而函數_e也是一個函數運算式,但我們將在VO裡 面找到,這是因為把它賦值給變數e了,它是通過e來擷取的。函式宣告和函數運算式在後面會詳細討論。這些結束後就進入了處理上下文代碼的第二個階段--代 碼執行階段。
代碼執行
在這個時候,AO/VO已經包含了這些屬性(雖然不是所有的屬性都有了我們傳遞的真實值,但大部分已經有了初始的值undefined).同樣的例子,在代碼解析時AO/VO做如下的修正:
AO['c'] = 10;
AO['e'] = <reference to FunctionExpression "_e">;
我們還要注意的這個函數運算式_e僅僅只存在於記憶體中,因為儲存在在已聲明的變數e裡。但是函數運算式x沒有在AO/VO中,如果我們在定義之前或定義之 後調用x函數,我們將會得到錯誤:"x" is not defined.未儲存的函數運算式僅能在它定義的地方調用或者遞迴的調用。
一個經典執行個體:
alert(x)//function x(){}
var x=10;
alert(x);//10
x=20;
function x(){}
alert(x);//20
為什麼一開始彈出X是一個函數,且在聲明之前就能過擷取了?為什麼不是10或者20?因為,根據規則—在進入上下文之前VO是被函式宣告填充的。與此同 時,這裡有一個變數聲明x,但我們上面已經提到,語義化的變數聲明階段在函式宣告和形參聲明之後。在這期間變數還不能和已經聲明的函數和形參名稱衝突。因 此,在進入VO上下文時:
VO={};
VO['x']=<reference to FunctionDeclaration "x">
//var x=10;
//if function "x"還沒定義,"x"為未定義。但是在這種情況下,變數聲明不能干擾同名的函數。
VO['x']=<值沒有被破壞,任然是function>
在代碼執行階段,VO修正如下
VO['x']=10;
VO['x']=20;
我們在第二和第三個alert出的結果。
在下面的例子中在進入上下文階段我們再次看到變數放入了VO中(因此,else從不被執行,但儘管如此,變數b還是存在VO中):
if(true){
var a=1;
}else{
var b=1;
}
alert(a);//1
alert(b);//undefined but not "b is not defined"
關於變數
許多關於javascript的文章甚至是書本說道:"使用var關鍵字(在全域執行環境)和不使用var關鍵字(在任何地方)聲明全域變數是可能的"。其實不是這樣的。請記住:變數只能通過var關鍵字聲明。
像這樣賦值:a=10;僅僅建立了全域對象的新屬性(而不是變數)。在這種意義下“Not the variable”並不是不能被改變的,但是在ECMAScript的變數概念下(由於VO(globalContext)===global,我們記住 了嘛?),它成為了全域對象的屬性。
不同之處在下面(通過例子來展示)
alert(a);//undefined
alert(b);//b is not defined
b=10;
var a=20;
所有的都依賴於VO和他的修正階段(進入執行內容和代碼執行階段):
進入上下文:
1 VO = {a: undefined};
我們看到在這個階段這裡沒出現任何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
關於變數這裡有更重要的一點。變數和簡單的屬性不同,有{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
記住:在ES5中{DontDelete}重新命名為[[Configureable]],並能通過Object.defineProperty方法手工管 理。然而有一種執行內容中這個規則是不起作用的。他就是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_屬性設定為了全域內容相關的變數對象也就是全域對象。然而在SpiderMonkey用同一種方式擷取使用中的物件是不可能的:依據不同的版本,內建函式的_parent_返回null或者全域對象。
在Rhino中,允許通過同樣的方式擷取使用中的物件:
var global=this;
var a=10;
(function foo(){
var y=20;
//"foo"函數內容相關的使用中的物件
var AO=(function(){})._parent_;
alert(AO.y);//20
//當前使用中的物件的_parent_已經變成了全域對象。這樣變數對象的一個特殊的鏈就形成了,就是所謂的範圍鏈
alert(AO._parent_===global);//true
alert(AO._parent_.x);//10
})()
總結
在這篇文章中我們繼續深入的與執行內容有關的對象。我希望這些材料是有用的而且講清楚了某些你以前覺得模稜兩可的方面。以後的計劃,在下面的章節中將會講到範圍鏈,確定標示符,最終是閉包。