基本概念:
执行环境(执行上下文)(Execution context EC):一个环境,或者说是一个作用域和里面的代码,从这些里面正在执行的代码向外(上下)看就是EC了。这个执行环境在js中是通过栈来实现。由于js中没有块级作用域,所以在这个栈中存在的只有两种执行环境,全局和局部。在js中基本执行环境就是全局的,也就是说在栈底。另一种就是本文涉及的来源于函数调用的局部执行环境。
js中函数执行环境的构成:
- 变量对象(Variable Object, VO),包含这个执行环境中的,函数形参(function formal parameters),函数声明(FunctionDeclaration, FD),变量声明(var, VariableDeclaration)(该顺序代表构建顺序)
- [[Scope]]属性,保存着作用域链,末尾指向全局VO,前方指向局部的AO。可理解为指针数组,从前往后访问。越外层的作用域越靠后。
- this指针,指向一个环境变量
执行环境的生命周期为:
- 创建阶段,在这个阶段,执行环境会1、建立作用域链 2、创建变量对象(提升在这发生) 3、确定this的指向。
- 执行阶段,变量赋值,函数引用,执行代码。
js的解释执行默认在全局执行环境中。当函数被调用时,会创建局部的执行环境。局部的执行环境压栈进入执行阶段。这时变量对象(VO)被激活,变为活动对象(Activation Object, AO)。这时VO中的数据才能被访问到。转变前后的差别如下:
AO = VO + function parameters + arguments。多了函数执行时的实参和arguments。arguments的属性值是Arguments对象。
闭包:
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式是在一个函数内部创建另一个函数。如下
function createComparisonFunction(propertyName) { return function(object1, object2){ var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2){ return -1; } else if (value1 > value2){ return 1; } else { return 0; } }; }
var compare = createComparisonFunction("name");
var result = compare({ name: "Nicholas" }, { name: "Greg" });
整体的函数的意思是外部函数的参数决定内部函数参数的取值。显然内部函数中访问了外部函数的参数,而这里存在的问题是,即便内部函数被返回了,而且在其他地方调用了,它任然能够访问到变量propertyName,当然这时访问到的的propertyName的最后的一个值。之所以能够访问这个变量,是因为内部函数的作用域链中包含外部函数的作用域。
当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后使用arguments和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的更外部的活动对象处于第三位,...直至终点的全局作用域。而我们知道,要找一个标识符总是从小范围到大范围,也就是从作用域链的前端到后端。如下函数及其作用域链:
function compare(value1, value2){ if (value1 < value2){ return -1; } else if (value1 > value2){ return 1; } else { return 0; } } var result = compare(5, 10);
上述函数的定义并在全局作用域中调用。当调用发生时,就会像上述的一样创建执行环境。在执行环境中创建变量对象VO,然后通过复制函数的[[Scope]]属性生成部分作用域链再将VO接在作用域链的前端,完成作用域链的创建,最后确定this的指向。VO在函数内代码执行时会被激活为AO,后的部分统一用AO指代AO和VO。作用域链的本质上是一个指向变量对象的指针列表。当使用标识符时,就从前端到后端查找。这里就是从函数的AO到全局查找。
一般来说,当函数执行完毕后局部活动对象就会被销毁,内存中仅保存全局作用域。但闭包不同。当在一个函数内定义另一个函数时。内部函数的作用域链的组成,从前到后是1、内部函数的AO,2、外部函数的AO,3、全局的VO。用图表示如下。
这个图的全局变量对象少了var compare,闭包的活动对象中少了var value1和var value2要注意,因为匿名函数是用函数表达式创建没有赋值给属性,所以外层活动对象中没有关于匿名函数的属性。图中的arguments对应的是传递进入的实参。当执行代码
var compare = createComparisonFunction("name"); var result = compare({ name: "Nicholas" }, { name: "Greg" });
外部的函数将内部的匿名函数返回到compare中后,因为这里的匿名函数是函数表达式,所以该函数在执行return时创建。同时拥有了作用域链。当return语句执行完,即外部函数执行结束,执行环境被弹出执行栈后。外部函数的执行环境本该被销毁。但是因为内部函数(此时为var compare)的作用域链仍然在引用外部函数的AO。其结果就导致虽然外部函数的执行环境被销毁,但它的AO仍然会留在内存中,直至匿名函数被销毁后,外部函数的AO才会被销毁。只需compare = null; 就能让匿名函数被销毁。此处设置为null,相当于通知垃圾回收例程将其清除。
对于闭包,因为闭包所保存的是整个变量对象链,而不是某个特殊的变量。所以闭包只能取得外部函数中任何变量的最后一个值(当然我们可以操作外部函数的变量的,例如赋值,修改等)。如下
function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = function(){ //创建了一个函数,指针保存在result[i]中,i不需要当作参数传入也能访问。 return i; }; } return result; }
该函数会返回一个函数数组。其中每个函数在调用时都会返回10,因为每个函数的作用域链中保存的外部函数的AO都是同一个,其中的变量i也都是同一个。然后都是在退出外部函数后才执行,所以一定时10。但是可以修改为
function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = function(num){ return function(){ return num; }; }(i); //调用函数 } return result; }
这里的区别是这里直接调用了函数。我们知道函数表达式function(num){}的返回值是一个函数的指针,然后再函数的指针上使用(),该函数指针就变成了函数的调用。所以这里直接是定义并调用了函数。
闭包的this对象:(当不使用this的时候,就按作用域链访问,使用this的时候才需要考虑环境)this对象是在运行时基于函数的执行环境绑定的。全局环境下this为window,当函数作为某个对象的方法调用时,this等于这个对象。一定要注意区分到底是谁调用了闭包。常常可能是window在调用,下面的保存this的例子可以调整闭包中的this。值得注意的是arguments也存在同样的问题。
var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ var that = this; //保存this的值 return function(){ return that.name; //因为有作用域链,所以可以访问that的值 }; } this.getNameFunc()(); }; alert(object.getNameFunc()()); //"My Object"
如果将函数中的var that = this去掉,匿名函数中换成this.name其结果为The Window,下面的例子没有闭包的事,只是看看this的变化。
var name = "The Window"; var object = { name : "My Object", getName: function(){ return this.name; } }; object.getName(); //"My Object" (object.getName)(); //"My Object" (object.getName = object.getName)(); //"The Window"
对于第二个,(object.getName)这部分相当于先取引用,然后再执行。调用它的还是object。对于第三个,这里和Java中一样,赋值表达式的返回值为所赋的值(就是等号左边的值)所以这里相当于想办法抽出了函数的指针,然后再全局作用域下调用。
内存泄漏(下面的说明的内存泄漏在IE9时就不存在了,因为IE9把BOM和DOM对象都转换成了正真的js对象):js有自动的垃圾回收机制,而对于自动的垃圾回收主要有两种实现,1、标记清除,在标记阶段,从根开始遍历,将能访问到的对象都加上一个标记,说明该对象可达。清除阶段,对堆从头到尾挨个线性遍历,如果有对象没有标记,就将其内存回收。并且清除所有标记,一边下一次的标记清除。2、引用计数,原理是当声明了一个变量,并将一个引用类型的值赋给该变量是,则该引用类型的值的引用次数为1。如果该引用又赋给另一个变量则引用次数加1,如果其中的一个变量取了另一个值,则该引用减1,当为0时,则回收其内存。引用计数看起来很好用,但是却不能处理循环引用的情况,如下。
function problem(){ var objectA = new Object(); var objectB = new Object(); objectA.someOtherObject = objectB; objectB.anotherObject = objectA; }
由于AB的相互引用导致这两个对象的引用值为2。在标记因为这两者在离开函数的局部执行环境后,两者都不可达,所以不成问题。所以主流的浏览器采用的都是标记清除或类似的垃圾收集策略。但是我们在编写js时常用的BOM和DOM是用C++以COM(Component Object Model,组建对象模型)对象的形式实现的。而COM对象的垃圾收集机制是引用计数。当js和COM对象循环引用时
function assignHandler(){ var element = document.getElementById("someElement"); element.onclick = function(){ alert(element.id); }; }
首先element的引用数至少为1,所以element占用的内存不会被回收。但为什么闭包在结束后不能被回收,现在还不知道,可能标记清除的时候因为element的可达,使得闭包也可达了。