來源:仗劍走天涯!
關于javascript的作用域的一些總結,主要參考以上文章,加上自己的整理的理解。
近日對javascript的作用域,以及它的運行過程,感覺不甚了解,尤其在使用閉包的時候,感覺都是模糊不清的。
與是在網上一探究竟,可是沒想,網上的大多文章都是含糊其次,且大多雷同,經多方查找,加上自己看一些書。
最終對javascript有一絲絲了解。記錄下來,以防忘記。
要徹底了解javascript的作用域就要了解一些概念。
1 運行期上下文
在javascript中,只有函數能夠創建出獨立的作用域,需要注意的是for循環是不能創建的,否則你的代碼就可能得到意想不到的結果。
4for(var k in {a:1, b:2}) {
alert(k);
}
alert(k);
即使循環結束,一樣可以alert出k的值.
2變量對象以及活動對象(VO/AO)
變量對象(縮寫為VO)是一個與執行上下文相關的特殊對象,它存儲著在上下文中聲明的以下內容:
變量 (var,變量聲明);
函數聲明 (FunctionDeclaration,縮寫為FD);
函數的形參
只有全局上下文的變量對象允許通過VO的屬性名稱來間接訪問(因為在全局上下文里,全局對象自身就是變量對象),在其它上下文中是不能直接訪問VO對象的,因為它只是內部機制的一個實現,通常使用AO(activation object來保存變量)。
VO保存變量對象示例:
6var a =10;
functiontest(x) {
var b =20;
};
test(30);
對應的變量對象是:
// 全局上下文的變量對象
9VO(globalContext) = {
a:10,
test:
};
// test函數上下文的變量對象
VO(test functionContext) = {
x:30,
b:20
};
2.1全局上下文中的變量對象(VO)
首先,我們要給全局對象一個明確的定義:
全局對象(Global object) 是在進入任何執行上下文之前就已經創建了的對象;
這個對象只存在一份,它的屬性在程序中任何地方都可以訪問,全局對象的生命周期終止于程序退出那一刻。
全局對象初始創建階段將Math、String、Date、parseInt作為自身屬性,等屬性初始化,同樣也可以有額外創建的其它對象作為屬性(其可以指向到全局對象自身)。例如,在DOM中,全局對象的window屬性就可以引用全局對象自身(當然,并不是所有的具體實現都是這樣):
7global = {
Math: <...>,
String:<...>
window: global//引用自身
};
當訪問全局對象的屬性時通常會忽略掉前綴,這是因為全局對象是不能通過名稱直接訪問的。不過我們依然可以通過全局上下文的this來訪問全局對象,同樣也可以遞歸引用自身。例如,DOM中的window。綜上所述,代碼可以簡寫為:
16String(10);// 就是global.String(10);
// 帶有前綴
window.a =10;// === global.window.a = 10 ===global.a = 10;
this.b =20;//global.b = 20;
alert(this=== window);
這里可以看到,用winodow調用其實是用的VO中的一個屬性,而用this,則是用的全局變量。
因此,回到全局上下文中的變量對象——在這里,變量對象就是全局對象自己:
VO(globalContext) === global;
非常有必要要理解上述結論,基于這個原理,在全局上下文中聲明的對應,我們才可以間接通過全局對象的屬性來訪問它(例如,事先不知道變量名稱)。
var a =newString('test');
alert(a);// 直接訪問,在VO(globalContext)里找到:test
alert(window['a']);// 間接通過global訪問:global === VO(globalContext): test
alert(a ===this.a);// true
var aKey ='a';
alert(window[aKey]);// 間接通過動態屬性名稱訪問:test
2.2函數上下文中的變量對象(AO)
在函數執行上下文中,VO是不能直接訪問的,此時由活動對象(activation object,縮寫為AO)扮演VO的角色。
VO(functionContext)=== AO;
活動對象是在進入函數上下文時刻被創建的,它通過函數的arguments屬性初始化。arguments屬性的值是Arguments對象:
3AO = {
arguments:
};
Arguments對象是活動對象的一個屬性,它包括如下屬性:
callee — 指向當前函數的引用
length — 真正傳遞的參數個數
properties-indexes (字符串類型的整數) 屬性的值就是函數的參數值(按參數列表從左到右排列)。 properties-indexes內部元素的個數等于arguments.length. properties-indexes 的值和實際傳遞進來的參數之間是共享的。
例如:
21function foo(x, y, z) {
// 聲明的函數參數數量arguments (x, y, z)
alert(foo.length);// 3
// 真正傳進來的參數個數(only x, y)
alert(arguments.length);// 2
// 參數的callee是函數自身
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,和參數的第3個索引值是不共享的
z =40;
alert(arguments[2]);// undefined
arguments[2] =50;
alert(z);// 40
}
foo(10,20);
這個例子的代碼,在當前版本的Google Chrome瀏覽器里有一個bug — 即使沒有傳遞參數z,z和arguments[2]仍然是共享的。
3 作用域鏈
從上面可以知道,每個上下文擁有自己的變量對象:對于全局上下文,它是全局對象自身;對于函數,它是活動對象。
9var x =10;
function foo() {
var y =20;
function bar(){
alert(x +y);
}
returnbar;
}
foo()();// 30
作用域鏈正是內部上下文所有變量對象(包括父變量對象)的列表。此鏈用來變量查詢。即在上面的例子中,“bar”上下文的作用域鏈包括AO(bar)、AO(foo)和VO(global)。這里的AO(foo)和VO(global)都是來自父變量對象.
1
作用域鏈與一個執行上下文相關,變量對象的鏈用于在標識符解析中變量查找。
函數上下文的作用域鏈在函數調用時創建的,包含活動對象和這個函數內部的[[scope]]屬性。
9在上下文中示意如下:
activeExecutionContext= {
VO: {...},//or AO? 上下文初始化的VO/AO
this: thisValue,
Scope: [// Scope chain,作用域鏈
// 所有變量對象的列表
// for identifiers lookup
]
};
其scope定義如下:
Scope= AO + [[Scope]]
這種聯合和標識符解析過程,我們將在下面討論,這與函數的生命周期相關。
對于Scope = AO + [[Scope]],在函數被調用的時候,上下文會創建一個Scope China,也就是Scope屬性,然后初始化為函數的內部屬性,也就是函數內部屬性[[Scope]],再將進入上下文時創建的VO/AO,壓入Scopechina的最前端。
3.1函數創建
函數的[[scope]]屬性是所有父變量對象的層級鏈,處于當前函數上下文之上,在函數創建時(函數生命周期分為函數創建和函數調用階段)存于其中。函數能訪問更高一層上下文的變量對象,這種機制是通過函數內部的[[scope]]屬性來實現的。
注意重要的一點--[[scope]]在函數創建時被存儲--靜態(不變的),永遠永遠,直至函數銷毀。即:函數可以永不調用,但[[scope]]屬性已經寫入,并存儲在函數對象中。由于是靜態存儲,再配合上內部函數的[[scope]]屬性是所有父變量的層級鏈,就導致了閉包的存在。如下:
9var x =10;
function foo() {
alert(x);
}
(function () {
var x =20;
foo();// 10,but not 20,這里會訪問foo中的[[scope]]的VO中的x
})();
這個例子也清晰的表明,一個函數(這個例子中為從函數“foo”返回的匿名函數)的[[scope]]持續存在,即使是在函數創建的作用域已經完成之后。
另外一個需要考慮的是--與作用域鏈對比,[[scope]]是函數的一個屬性而不是上下文??紤]到上面的例子,函數“foo”的[[scope]]如下:
foo.[[Scope]] = [ globalContext.VO// === Global ];
舉例來說,我們用通常的ECMAScript 數組展現作用域和[[scope]]。
3.2函數激活
正如在定義中說到的,進入上下文創建AO/VO之后,上下文的Scope屬性(變量查找的一個作用域鏈)作如下定義:
Scope= AO|VO + [[Scope]]
上面代碼的意思是:活動對象是作用域數組的第一個對象,即添加到作用域的前端。
Scope= [AO].concat([[Scope]]);
這個特點對于標示符解析的處理來說很重要。
標示符解析是一個處理過程,用來確定一個變量(或函數聲明)屬于哪個變量對象。
這個算法的返回值中,我們總有一個引用類型,它的base組件是相應的變量對象(或若未找到則為null),屬性名組件是向上查找的標示符的名稱。
標識符解析過程包含與變量名對應屬性的查找,即作用域中變量對象的連續查找,從最深的上下文開始,繞過作用域鏈直到最上層。
這樣一來,在向上查找中,一個上下文中的局部變量較之于父作用域的變量擁有較高的優先級。萬一兩個變量有相同的名稱但來自不同的作用域,那么第一個被發現的是在最深作用域中。
我們用一個稍微復雜的例子描述上面講到的這些。
10var x =10;
functionfoo() {
var y =20;
function bar() {
var z =30;
alert(x +? y +z);
}
bar();
}
foo();//60
對此,我們有如下的變量/活動對象,函數的的[[scope]]屬性以及上下文的作用域鏈:
全局上下文的變量對象是:
4globalContext.VO=== Global = {
x:10
foo:
};
在“foo”創建時,“foo”的[[scope]]屬性是:
3foo.[[Scope]]= [
globalContext.VO
];
在“foo”激活時(進入上下文),“foo”上下文的活動對象是:
4fooContext.AO= {
y:20,
bar:
};
“foo”上下文的作用域鏈為:
6fooContext.Scope= fooContext.AO + foo.[[Scope]]// i.e.:
fooContext.Scope= [
fooContext.AO,
globalContext.VO
];
內部函數“bar”創建時,其[[scope]]為:
4bar.[[Scope]]= [
fooContext.AO,
globalContext.VO
];
在“bar”激活時,“bar”上下文的活動對象為:
3barContext.AO= {
z:30
};
“bar”上下文的作用域鏈為:
7barContext.Scope= barContext.AO + bar.[[Scope]]// i.e.:
barContext.Scope= [
barContext.AO,
fooContext.AO,
globalContext.VO
];
對“x”、“y”、“z”的標識符解析如下:
11-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
4閉包
創建閉包的常見方式是在一個函數內部創建另一個函數,以create ()函數為例:
10function create(){
var x =0
returnfunction(){
alert(++x);
};
}
var c = create();
c();//alert 1not 0
c();//alert 2not 0
c();//alert 3not 0
上面代碼中的內部函數(一個匿名函數),訪問了外部函數中的變量x。即使這個內部函數被返回了,且在其他地方被調用了,但它仍可訪問變量x.之所以還能夠訪問這個變量,是因為內部函數的作用域鏈中包含create ()的作用域。
當某個函數第一次被調用時,會創建一個執行環境及相應的作用域鏈,并把一個特殊的內部屬性(即[[Scope]] ) 賦值給作用域鏈。然后使用this、arguments和其他命名參數的值來初始化函數的活動對象。然后把該活動對象壓入作用域鏈的最前端。所以在作用域鏈中,父級活動對象始終處于第二位,直到作用域鏈終點的全局執行環境。
在函數執行過程中,為讀取和寫入變量的值,需要在作用域中查找變量:
9function compare(value1,value2){
if(value2value2){
return1;
}
else{
return0;
}
}
var result =compare(5,10);
上面的代碼先定義了compare()函數,又在全局作用域調用了它。第一次調用compare()時,會創建一個包含this、arguments、value1、value2的活動對象。全局執行環境的變量對象(包含this,result,compare)在compare()執行環境的作用域中處于第二位,如圖:
此后又有一個活動對象(在此作為變量對象使用)被創建并被推入執行環境作用域鏈的前端。對于這個例子中compare()函數的執行環境而言,其作用域鏈中包含兩個變量對象:本地活動對象和全局變量對象。
作用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象
一般來講在當函數執行完畢后,局部活動對象就會被銷毀,內在中僅保存全局作用域,但閉包情況有所不同。另一個函數內部定義的函數會將包含函數的活動對象添加到它的作用域鏈中。因此,createComparisonFunction()函數內定義的匿名函數的作用域鏈中,實際上將會包含外部函數createComparisonFunction()的活動對象,
9function createComparisonFunction(propertyName){
returnfunction(object1,object2){
varvalue1 = object1[propertyName];
varvalue1 = object2[propertyName];
if(value1 < value2){return-1;}
elseif (value1>value2){return1;}
else{return0;}
};
}
2var compare =createComparisonFunction(name);
var result =compare({name:'Nicholas},{name:Greg});
5閉包和變量
看如下代碼:
10function fn(){
var result =newArray();
for(vari=0;i<10;i++){
result[i]= function(){returni;}
}
returnresult;
}
var funcs = fn();
for(var i=0;i);
}
這段代碼代碼可能說是堪稱經典。你也許知道結果會輸出10個10。
這里的原因還是因為作用域的問題。
在fn 被調用結束后,那么在fn的作用鏈的AO中保存i的值為10。
主要的問題就是result[i]的方法的作用鏈中保存了父級活動對象。那么當激活result方法時,會先去自已的活動對象中搜索,當沒有發現表示符時,則會去父級活動對象中搜索,當搜索到i的時候,這個i已經是10了。這就是為什么結果都為10。當然需要如果需要得到你想要的結果,那么可以采用如下的方式。
5result[i] = function(num){
returnfunction(){
returnnum;
}
}(i);
這里為什么能得出正確的結果呢?其實這里是將修改了result的活動對象。
第一個result的函數的活動對象中多了一個 num屬性,而這個屬性是用一個自啟動函數傳入的。每次激活函數的時候直接在自已的活動對象中去搜索到num就返回了,不再去父級變量中搜索了。這樣就能結果問題了。
6 javascript 代碼執行過程
javascript中的function的一些東西這里就不說了。
關于function對象的[[scope]]是一個內部屬性。
Functiion對象在創建的時候會自動創建一個[[scope]]內部屬性,且只有js引擎才能夠去訪問。同時也會創建一個scope chian的作用域鏈。且[[scope]]鏈接到scope china中。
3function add(){
Var name = “hello”;
}
那么如上:
Function add 在創建的時候就會創建一個[[scope]]的屬性,且指向scope chian.由于是在全局的環境中,那么scope chian剛指向window action object.(這里不是很清楚).
執行此函數時會創建一個稱為“運行期上下文(execution context)”的內部對象,運行期上下文定義了函數執行時的環境。每個運行期上下文都有自己的作用域鏈,用于標識符解析,當運行期上下文被創建時,而它的作用域鏈初始化為當前運行函數的[[Scope]]所包含的對象。
運行期上下文的代碼被分成兩個基本的階段來處理:
進入執行上下文
執行代碼
變量對象的修改變化與這兩個階段緊密相關。
注:這2個階段的處理是一般行為,和上下文的類型無關(也就是說,在全局上下文和函數上下文中的表現是一樣的)。
6.1 進入執行上下文
當進入執行上下文(代碼執行之前)時,VO/AO里已經包含了下列屬性(前面已經說了):
函數的所有形參(如果我們是在函數執行上下文中)
— 由名稱和對應值組成的一個變量對象的屬性被創建;沒有傳遞對應參數的話,那么由名稱和undefined值組成的一種變量對象的屬性也將被創建。
所有函數聲明(FunctionDeclaration,FD)
—由名稱和對應值(函數對象(function-object))組成一個變量對象的屬性被創建;如果變量對象已經存在相同名稱的屬性,則完全替換這個屬性。
所有變量聲明(var,VariableDeclaration)
— 由名稱和對應值(undefined)組成一個變量對象的屬性被創建;如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性。
讓我們看一個例子:
7function test(a, b) {
var c =10;
function d(){}
var e =function _e() {};
(function x(){});
}
test(10);// call
當進入帶有參數10的test函數上下文時,AO表現為如下:
7AO(test) = {
a:10,
b: undefined,
c: undefined,
d:
e: undefined
};
注意,AO里并不包含函數“x”。這是因為“x”是一個函數表達式(FunctionExpression, 縮寫為 FE) 而不是函數聲明,函數表達式不會影響VO。 不管怎樣,函數“_e” 同樣也是函數表達式,但是就像我們下面將看到的那樣,因為它分配給了變量“e”,所以它可以通過名稱“e”來訪問。
這之后,將進入處理上下文代碼的第二個階段— 執行代碼。
6.2 代碼執行
這個周期內,AO/VO已經擁有了屬性(不過,并不是所有的屬性都有值,大部分屬性的值還是系統默認的初始值undefined )。
還是前面那個例子, AO/VO在代碼解釋期間被修改如下:
2AO['c'] =10;
AO['e'] = ;
再次注意,因為FunctionExpression“_e”保存到了已聲明的變量“e”上,所以它仍然存在于內存中。而FunctionExpression “x”卻不存在于AO/VO中,也就是說如果我們想嘗試調用“x”函數,不管在函數定義之前還是之后,都會出現一個錯誤“x is not defined”,未保存的函數表達式只有在它自己的定義或遞歸中才能被調用。
另一個經典例子:
6alert(x);// function
var x =10;
alert(x);// 10
x =20;
function x() {};
alert(x);// 20
為什么第一個alert “x” 的返回值是function,而且它還是在“x” 聲明之前訪問的“x” 的?為什么不是10或20呢?因為,根據規范函數聲明是在當進入上下文時填入的; 同意周期,在進入上下文的時候還有一個變量聲明“x”,那么正如我們在上一個階段所說,變量聲明在順序上跟在函數聲明和形式參數聲明之后,而且在這個進入上下文階段,變量聲明不會干擾VO中已經存在的同名函數聲明或形式參數聲明,因此,在進入上下文時,VO的結構如下:
10VO = {};
VO['x'] =
// 找到var x = 10;
// 如果functionx沒有已經聲明的話
// 這時候x的值應該是undefined
// 但是這個case里變量聲明沒有影響同名的function的值
VO['x'] =
緊接著,在執行代碼階段,VO做如下修改:
VO['x'] =10;
VO['x'] =20;
我們可以在第二、三個alert看到這個效果。
在下面的例子里我們可以再次看到,變量是在進入上下文階段放入VO中的。(因為,雖然else部分代碼永遠不會執行,但是不管怎樣,變量“b”仍然存在于VO中。)
7if(true) {
var a =1;
}else{
var b =2;
}
alert(a);//
alert(b);// undefined,不是b沒有聲明,而是b的值是undefined
7 關于變量
通常,各類文章和JavaScript相關的書籍都聲稱:“不管是使用var關鍵字(在全局上下文)還是不使用var關鍵字(在任何地方),都可以聲明一個變量”。請記住,這是錯誤的概念:
任何時候,變量只能通過使用var關鍵字才能聲明。
上面的賦值語句:
a= 10;
這僅僅是給全局對象創建了一個新屬性(但它不是變量)。“不是變量”并不是說它不能被改變,而是指它不符合ECMAScript規范中的變量概念,所以它“不是變量”(它之所以能成為全局對象的屬性,完全是因為VO(globalContext) === global,大家還記得這個吧?)。
讓我們通過下面的實例看看具體的區別吧:
4alert(a);// undefined
alert(b);// b 沒有聲明
b =10;
var a =20;
所有根源仍然是VO和進入上下文階段和代碼執行階段:
進入上下文階段:
3VO = {
a: undefined
};
我們可以看到,因為“b”不是一個變量,所以在這個階段根本就沒有“b”,“b”將只在代碼執行階段才會出現(但是在我們這個例子里,還沒有到那就已經出錯了)。
讓我們改變一下例子代碼:
4alert(a);// undefined, 這個大家都知道,
b =10;
alert(b);// 10, 代碼執行階段創建
var a =20;
關于變量,還有一個重要的知識點。變量相對于簡單屬性來說,變量有一個特性(attribute):{DontDelete},這個特性的含義就是不能用delete操作符直接刪除變量屬性。
13a =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}特性,可以被刪除。
如果能認真看完博客,相信你一定對javascript很感興趣。
文檔如果對你有一絲絲的幫助,那么恭喜。