下文根据汤姆大叔的深刻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.在代码中引用了自由变量