《JS高级程序设计-第3版》的读书笔记关于闭包和内存泄漏

基本概念:

执行环境(执行上下文)(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的可达,使得闭包也可达了。

posted @ 2019-07-31 21:10  缓步徐行静不哗  阅读(233)  评论(0编辑  收藏  举报