下文根據湯姆大叔的深刻javascript系列文章刪改,若是想深刻理解請閱讀湯姆大叔的系列文章。
http://www.cnblogs.com/TomXu/...javascript
變量對象(縮寫爲VO)是一個與執行上下文相關的特殊對象,它存儲着在上下文中聲明的如下內容: 變量 (var, 變量聲明); 函數聲明 (FunctionDeclaration, 縮寫爲FD); 函數的形參
咱們能夠用普通的ECMAScript對象來表示一個變量對象:html
VO = {};
VO是執行上下文的屬性(property),因此:java
activeExecutionContext = { VO: { // 上下文數據(var, FD, function arguments) } };
只有全局上下文的變量對象容許經過VO的屬性名稱來間接訪問(由於在全局上下文裏,全局對象自身就是變量對象),在其它上下文中是不能直接訪問VO對象的,由於它只是內部機制的一個實現。閉包
只有全局上下文的變量對象容許經過VO的屬性名稱來間接訪問ide
在全局上下文中,有函數
VO(globalContext) === global;
由於咱們在全局上下文中聲明的變量等都是存在全局的變量對象中,而在全局上下文中的全局變量對象又是全局對象自己。因此咱們能夠經過VO的屬性名稱間接訪問this
var a = new String('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"
在函數執行上下文中,VO是不能直接訪問的,此時由活動對象(activation object,縮寫爲AO)扮演VO的角色。code
VO(functionContext) === AO;
在理解函數上下文中的變量對象時,咱們經過處理上下文代碼的2個階段來進行理解htm
1.進入執行上下文 2.執行代碼
進入執行上文文的時候,也便是代碼執行以前,此時VO包含了下列屬性對象
函數形參 函數聲明 變量聲明
其中,函數聲明的等級最高,而後是函數形參,最後纔是變量聲明。越高等級的聲明能夠覆蓋低等級的聲明。
這個週期內,AO/VO已經擁有了屬性(不過,並非全部的屬性都有值,大部分屬性的值仍是系統默認的初始值undefined )。這個時候會進行賦值操做以及執行代碼。
alert(x); // function var x = 10; alert(x); // 10 x = 20; function x() {}; alert(x); // 20
在進入上下文階段,因爲函數具備最高的級別,因此第一次alert(x)輸出的是函數。以後進行變量賦值,分別alert 10 20。
function bar (x){ alert(x); var x = 2; } bar(3); //3
因爲形參聲明比變量聲明級別高,因此alert(3),由於在進入執行上下文時變量沒法覆蓋形參聲明,因此輸出的是3而不是undefined。
alert(a); // undefined alert(b); // "b" 沒有聲明,報錯 b = 10; var a = 20;
函數上下文的做用域鏈在函數調用時建立的,包含活動對象和這個函數內部的[[scope]]屬性。函數上下文包括如下內容:
activeExecutionContext = { VO: {...}, // or AO this: thisValue, Scope: [ // Scope chain // 全部變量對象的列表 // for identifiers lookup ] };
其scope定義以下:
Scope = AO + [[Scope]]
[[scope]]是全部父變量對象的層級鏈,處於當前函數上下文之上,在函數建立時存於其中。
注意這重要的一點--[[scope]]在函數建立時被存儲--靜態(不變的),永遠永遠,直至函數銷燬。即:函數能夠永不調用,但[[scope]]屬性已經寫入,並存儲在函數對象中。
另一個須要考慮的是--與做用域鏈對比,[[scope]]是函數的一個屬性而不是上下文。
所以我我的的理解是做用域鏈應該是函數自己的活動對象+父級的變量對象。其中函數自己的活動對象老是排在第一位,在尋找標識符的時候,若是在當前活動對象找不到,那麼會遍歷做用域鏈上的父級變量對象。其中[[scope]]在函數建立時被存儲,與函數共存亡。
var x = 10; function foo() { alert(x); } (function () { var x = 20; foo(); // 10, but not 20 })();
說明函數的做用域鏈在函數建立的時候就已經定義好了,是靜態的,不由於調用的時候而改變。
var firstClosure; var secondClosure; function foo() { var x = 1; firstClosure = function () { return ++x; }; secondClosure = function () { return --x; }; x = 2; // 影響 AO["x"], 在2個閉包公有的[[Scope]]中 alert(firstClosure()); // 3, 經過第一個閉包的[[Scope]] } foo(); alert(firstClosure()); // 4 alert(secondClosure()); // 3
firstClosure和secondClosure兩個函數建立的時候,內部的變量x都是從父級函數foo的變量對象x中引用,因此其實兩個函數都是共享一個做用域,所以致使x變量共通了。
var data = []; for (var k = 0; k < 3; k++) { data[k] = function () { alert(k); }; } data[0](); // 3, 而不是0 data[1](); // 3, 而不是1 data[2](); // 3, 而不是2
解釋跟上面相似。function在建立的時候,內部的變量k經過訪問做用域鏈便是父級的變量對象k拿到,而當函數被調用的時候,for循環早已執行完畢,此時的K是3,因此三個函數調用的時候輸出的值都爲3。
var data = []; for (var k = 0; k < 3; k++) { data[k] = (function _helper(x) { return function () { alert(x); }; })(k); // 傳入"k"值 } // 如今結果是正確的了 data[0](); // 0 data[1](); // 1 data[2](); // 2
建立了一個匿名函數,經過把k變量做爲參數傳進去,這樣在執行function的時候,因爲內部的形參可以訪問到k變量,因此無需到父級做用域鏈上進行尋找,所以最後輸出達到預期目的。
這裏說明一下,開發人員常常錯誤將閉包簡化理解成從父上下文中返回內部函數,甚至理解成只有匿名函數才能是閉包。
ECMAScript中,閉包指的是:
1.從理論角度:全部的函數。由於它們都在建立的時候就將上層上下文的數據保存起來了。哪怕是簡單的全局變量也是如此,由於函數中訪問全局變量就至關因而在訪問自由變量,這個時候使用最外層的做用域。 2.從實踐角度:如下函數纔算是閉包: 1.即便建立它的上下文已經銷燬,它仍然存在(好比,內部函數從父函數中返回) 2.在代碼中引用了自由變量