小面网站建设,宁波网站怎么建设,wordpress如何加html,怎么做装修网站闭包真的是一个谈烂掉的内容。说到闭包#xff0c;自然就涉及到执行环境、变量对象以及作用域链。汤姆大叔翻译的《深入理解JavaScript系列》很好#xff0c;帮我解决了一直以来似懂非懂的很多问题#xff0c;包括闭包。下面就给自己总结一下。包括参考大叔的译文以及《Java…闭包真的是一个谈烂掉的内容。说到闭包自然就涉及到执行环境、变量对象以及作用域链。汤姆大叔翻译的《深入理解JavaScript系列》很好帮我解决了一直以来似懂非懂的很多问题包括闭包。下面就给自己总结一下。包括参考大叔的译文以及《JavaScript高级程序设计第3版》一些例子引用自它们。 附上大叔的链接《深入理解JavaScript系列》 一、执行环境或“执行上下文”意义一样 首先说下ECMAScript可执行代码的类型包括全局代码、函数代码、eval()代码。 每当执行流转到可执行代码时即会进入一个执行环境。活动的执行环境构成一个栈栈的底部始终是全局环境顶部是当前活动的执行环境。 ❶全局执行环境是最外围的一个执行环境。在浏览器中全局环境就是window对象因此所有全局变量和函数都是作为window对象的属性和方法创建的。 ❷每个函数都有自己的执行环境。当执行流进入一个函数时函数的环境被推入栈中。而在函数执行之后栈将其环境弹出把控制权返回给之前的执行环境。某个执行环境中的代码执行完后该环境销毁保存在其中的所有变量和函数定义也随之销毁。而全局执行环境直到应用程序退出才会被销毁。 ❸eval的执行环境与调用环境的执行环境相同。 二、变量对象 我们知道变量和执行环境有着密切的关系 var a 10; // 全局上下文中的变量(function () {var b 20; // function上下文中的局部变量
})();alert(a); // 10
alert(b); // 全局变量 b 没有声明 而且我们也知道在JS里没有块级作用域这一说法ES规范指出独立作用域只能通过函数function代码类型的执行环境创建。也就是说像for循环并不能创建一个局部环境 for (var k in {a: 1, b: 2}) {alert(k);
}alert(k); // 尽管循环已经结束但变量k依然在当前作用域 既然变量与执行环境相关那变量自己应该知道它的数据存放在哪里并知道如何访问。这就引出了“变量对象”这个概念。 每个执行环境都有一个与之关联的变量对象这个对象存储着在环境中定义的以下内容 1. 函数的形参2. var声明的变量3. 函数声明(不包括函数表达式) 举例来说用一个普通对象来表示变量对象它是执行环境的一个属性 执行环境 {变量对象:{//环境中的数据}
}; 例如 var a 10;function test(x) {var b 20;
};test(30); 对应的变量对象为 // 全局执行环境的变量对象
全局环境的变量对象 {a: 10,test: 指向test()函数
};// test函数执行环境的变量对象
test函数环境的变量对象 {x: 30,b: 20
}; 那么不同执行环境中的变量对象的初始化是怎样的呢下面详细看一下 ❶全局环境中的变量对象 先看下全局对象的明确定义 全局对象 是在进入任何执行环境之前就已经创建了的对象。这个对象只存在一份它的属性在程序中的任何地方都可以访问全局对象的生命周期终止于程序退出那一刻。 全局对象初始创建阶段将Math、String等作为自身属性初始化如下 globla {Math:String:......window:globla //引用自身
}; 在这里变量对象就是全局对象自己。 ❷函数环境中的变量对象 在函数执行环境中“活动对象” 扮演着变量对象这个角色。活动对象是在进入函数执行环境时创建的它通过函数的arguments属性初始化 活动对象 {arguments: //是个对象包括callee、length等属性
}; 理解了变量对象的初始化之后下面就是关于变量对象的核心了。 环境中的代码被分为两个阶段来处理进入执行环境 、执行代码。变量对象的修改变化与这两个阶段紧密相关。 这2个阶段的处理是一般行为和环境的类型无关即在全局环境和函数环境中的表现是一样的。 ①进入环境 当进入执行环境时代码执行之前变量对象已包含下列属性上面有提到 ①函数的所有形参如果是在函数执行环境中。因为全局环境没有形参。————由 形参名称 和 对应值 组成作为变量对象的属性。如果没有传递对应的参数将undefined作为对应值。②所有函数声明注意是声明函数表达式不算。————由 函数名 和 对应值函数对象组成作为变量对象的属性。如果变量对象已经存在同名的属性则覆盖这个属性。③所有变量声明由var声明的变量————由 变量名 和 对应值undefined 组成作为变量对象的属性。如果变量名与已经声明的形参或函数相同则变量声明不会干扰已经存在的这类属性。————注意此时的对应值是undefined。 让我们来看一个例子 function test(a, b) {alert(c); //undefinedalert(d); //function d() {}alert(e); //undefinedalert(x); //出错var c 10;function d() {}var e function _e() {};(function x() {});
}test(10); // 我们考虑当进入带有参数10的test函数环境时代码执行之前活动对象表现如下 活动对象(test) {a: 10,b: undefined, c: undefined,d:指向函数d,e: undefined
}; 注意活动对象里不包含函数x。这是因为x是一个函数表达式而不是函数声明函数表达式不会影响变量对象在这里是活动对象。函数_e同样是函数表达式但是我们注意到它分配给了变量e所以可以通过名称e来访问。 在这之后将进入处理代码的第二个阶段执行代码。 ②执行代码 这个阶段内变量/活动对象已经拥有了属性不过并不是所有属性都有值就像上面那个例子大部分属性的值还是系统默认的undefined。 继续上面那个例子活动对象在“执行代码”这个阶段被修改如下 AO(test) {a: 10,b: undefined, //没有相应该参数传入undefinedc: 10, //之前是undefinedd: 指向函数de: 指向函数表达式_e //之前是undefined
}; 注意此时函数表达式_e保存到了已声明的变量e上但函数表达式x本身不存在于活动对象中也就是说如果尝试调用函数x无论在函数定义之前或之后都会出现 “x is not defined”的错误。 理解了以上内容之后再来看一个例子 alert(x); // functionvar x 10;
alert(x); // 10x 20;function x() {};alert(x); // 20 为什么第一个alert(x)的值是function而且它还是在x声明之前访问的x为什么不是10或20呢 现在我们知道函数声明是在进入环境时填入活动对象的同一时间还有一个变量声明x但是正如前面所说变量声明在顺序上跟在函数声明和形参声明之后。即在进入环境阶段变量声明不会干扰变量对象中已经存在的同名函数或形参声明。所以就这个例子来说在进入环境时变量对象的结构如下 变量对象 {x:指向函数x//如果function x没有已经声明的话这时的x应该是undefined
}; 紧接着在代码执行阶段变量对象作如下修改 变量对象[x] 10;
变量对象[x] 20;
//可以在第二、三个alert看到这个结果 再看一个例子 if (true) {var a 1;
} else {var b 2;
}
//变量是在进入环境阶段放入变量对象的虽然else部分永远不会执行
//但是不管怎样变量b仍然存在于变量对象中。
alert(a); //1
alert(b); //undefined不是b未声明而是b的值是undefined 另外关于var声明变量和不用var声明 大叔的译文中指出任何时候变量只能通过var关键字才能声明。 像a 10;这仅仅是给全局对象创建了一个新属性但它不是变量。它之所以能成为全局对象的属性完全是因为全局对象全局变量对象。看例子 alert(a); // undefined
alert(b); // b 没有声明出错b 10;
var a 20; 进入环境阶段 变量对象 {a: undefined
}; 可以看到因为b不是一个变量所以在这个阶段根本就没有bb将只在代码执行阶段才会出现但在这里还未执行到那就出错了。 还有一个要注意的var声明的变量相对于属性如a 10;或window.a 10;变量的[[Configurable]]特性值为false即不能通过delete删除而属性则可以。 三、作用域链 现在我们已经知道一个执行环境的数据变量、函数声明和函数形参作为属性存储在变量对象中。 同时也知道变量对象在每次进入环境时创建并填入初始值值的更新出现在代码执行阶段。 下面的内容讨论作用域链。 如果要简要地描述并展示其重点那么作用域链大多数与内部函数相关。 我们可以创建内部函数甚至能从父函数中返回这些函数。 var x 10;function foo() { var y 20; function bar() {alert(x y);} return bar;
}foo()(); // 30 很明显每个环境拥有自己的变量对象对于全局环境它是全局对象自身对于函数它是活动对象。 作用域链正是内部环境所有变量对象包括父变量对象的列表。此链用来在标识符解析中变量查找。 作用域链本质上是一个指向变量对象的指针列表它只引用但不实际包含变量对象。 对于上面这个例子bar执行环境中的作用域链包括bar变量对象、foo变量对象、全局变量对象。 函数执行环境中的作用域链在函数调用时创建包含这个函数的活动对象和函数的[[scope]]属性。示例如下 活动的执行环境 {变量对象: {...}, // or 活动对象this: thisValue,Scope: [ // 作用域链// 它是所有变量对象的列表。]
}; 其中的Scope定义为Scope 被调用函数的活动对象 [[scope]]。 这种标识符的解析过程与函数的生命周期相关下面详细讨论。 1函数的生命周期 函数的生命周期分为创建和激活调用时两个阶段。 ❶函数创建 让我们先看看在全局环境中的变量和函数声明这里的变量对象就是全局对象自身我们懂的。 var x 10;function foo() {var y 20;alert(x y);
}foo(); // 30 函数激活时得到了正确的也是预期中的结果。但我们注意到变量y在函数foo中定义意味着它在foo的活动对象中但是x并未在foo环境中定义相应地它不会添加到foo的活动对象中。那么foo是如何访问到变量x的其实我们大都知道函数能访问更高一层的环境中的变量对象事实也是如此而这种机制正是通过函数内部的[[scope]]属性实现的。 [[scope]]是所有父变量对象的层级链处于当前函数环境在函数创建时存在于其中。 注意重要的一点[[scope]]属性在函数创建时被存储永远不变直到函数销毁。函数可以不被调用但这个属性一直存在。且与作用域链相比作用域链是执行环境的一个属性而[[scope]]是函数的属性。 上面的例子函数foo的[[scope]]如下 foo.[[Scope]] [全局执行环境.变量对象 // Global
]; 继续我们知道在函数调用时进入执行环境这时活动对象被创建this、作用域链被确定。下面详细考虑这个时刻。 ❷函数激活 正如上面提到的进入环境创建变量/活动对象之后环境的Scope属性即作用域链定义为Scope 变量/活动对象 [[scope]]。 这个定义意思是活动对象是被添加到[[scope]]前端在作用域链中处理第一位。这很重要对于标识符的查找是从自身变量对象开始的逐渐往父变量对象查找。 2通过构造函数创建的函数的[[scope]] 在上面的例子中我们看到在函数创建时函数获得[[scope]]属性该属性存储着所有父环境的变量/活动对象。但有一个例外那就是通过构造函数创建的函数。 var x 10;function foo() {var y 20;function barFD() { // 函数声明alert(x);alert(y);}var barFE function () { // 函数表达式alert(x);alert(y);};var barFn Function(alert(x); alert(y););barFD(); // 10, 20barFE(); // 10, 20barFn(); // 10, y is not defined}foo(); 从以上例子中我们看出问题所在通过构造函数创建的函数它的[[scope]]仅包含全局对象。 另外关于eval实践中很少用到eval但有一点提示eval代码的环境与当前的调用环境拥有相同的作用域链。 3延长作用域链 有两个能延长作用域链的方法with声明和catch语句。它们添加到作用域链的最前端比被调用函数的活动对象还要靠前。 如果发生其中一个作用域链作如下修改 Scope withObject|catchObject 活动/变量对象 [[Scope]] 看个例子 var x 10, y 10;with ({x: 20}) {var x 30, y 30;alert(x); // 30alert(y); // 30
}alert(x); // 10
alert(y); // 30//1. x 10,y 10;//2. 进入环境对象{x:20}添加到作用域链的前端。//3. 执行代码x为20变为30y为10变为30。//4.with声明完成后对象被移除那个因with对象而改变的x30也被移除。//最后两个alertx保持最初不变y在with里已发生改变。 四、闭包 到了这里其实如果对前面的[[scope]]和作用域链完全理解的话闭包也就懂了。 大叔的译文对闭包给出的2个定义是 ❶从理论角度所有函数都是闭包。因为它们在创建的时候就将所有父环境的数据保存起来了。哪怕是简单的全局变量也是如此因为在函数中访问全局变量就相当于在访问自由变量指不在参数声明也不在局部声明的变量这个时候使用最外层的作用域。 ❷从实践角度以下函数才算是闭包 ①即使创建它的环境销毁它仍然存在比如内部函数从父函数返回②在代码中引用了自由变量。 闭包的性能问题总被提及现在我们知道原因了创建闭包的父环境即使被销毁了但闭包仍然引用着父环境的变量对象也就是说需要继续维护着这个变量对象的内存。 下面我们再来具体看一下。 var x 10;function foo() {alert(x);
}(function (funArg) {var x 20;// 变量x在foo中静态保存的在该函数创建的时候就保存了funArg(); // 10, 而不是20})(foo); 我们已经知道创建foo函数的父级环境在这里是全局环境的数据是保存在foo函数的内部属性[[scope]]中的。 这里还要注意的是同一个父环境创建的闭包是共用一个[[scope]]属性的。也就是说某个闭包对其中[[scope]]的变量的修改会影响到其他闭包对其变量的读取。 var firstClosure;
var secondClosure;function foo() {var x 1;firstClosure function () { return x; };secondClosure function () { return --x; };x 2; // 影响x, 在2个闭包公有的[[Scope]]中
alert(firstClosure()); // 3, 通过第一个闭包的[[Scope]]
}foo();alert(firstClosure()); // 4
alert(secondClosure()); // 3 关于这个问题大叔的译文和《JS高级》里都有一个例子 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 这就是闭包共用一个[[scope]]的问题。可以按下面的方法解决 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 在上例中每次_helper都会创建一个新的变量对象其中含有参数x其值就是传递进来的k值。此时返回的函数的[[scope]]如下 data[0].[[Scope]] [... // 其它变量对象父级环境中的活动对象: {data: [...], k: 3},_helper环境中的活动对象: {x: 0}
];data[1].[[Scope]] [... // 其它变量对象父级环境中的活动对象: {data: [...], k: 3},_helper环境中的活动对象: {x: 1}
];data[2].[[Scope]] [... // 其它变量对象父级环境中的活动对象: {data: [...], k: 3},_helper环境中的活动对象: {x: 2}
]; 要注意的是如果在返回的函数中要获取k值那么该值还会是3。 五、小结 总结得好长啊好长。因为我觉得一口气将这几个点连在一起梳理一下比较好。 嗯就这样吧。转载于:https://www.cnblogs.com/no-particular/archive/2013/01/31/2887293.html