變量對象+做用域鏈+閉包

下文根據湯姆大叔的深刻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。

不使用var能夠聲明一個全局變量,這句話是錯誤的。

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.在代碼中引用了自由變量
相關文章
相關標籤/搜索