串讲-解释篇:作用域,作用域链,执行环境,变量对象,活动对象,闭包

这篇接:理论篇:作用域,作用域链,执行环境,变量对象,活动对象,闭包

看例子:

function compare(value1, value2) {
    if (value1 < value2) {
        return -1;
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}
//var result = compare(5, 10);

注意这里把 result 和 compare 函数执行的代码给注释掉了,也就是说,这里仅仅只有一个函数声明语句,在这个时候,就发生了下面的事情;

创建 compare() 函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。

这个时候的作用域链是这个样子:

image

截图:

 

函数执行时,代码就是这样:

function compare(value1, value2) {
    if (value1 < value2) {
        return -1;
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}
var result = compare(5, 10);

此时:

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

另一个说法:

函数执行时,就会创建一个执行环境,某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出——例如关闭网页或浏览器——时才会被销毁)。

当调用compare()函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。对于这个例子中compare()函数的执行环境而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

这个时候作用域链就是这样:

image

无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。

总结:

1、函数创建时就会创建一个包含全局变量对象(上面案例仅仅是全局)和外部函数变量对象上面案例只有全局,斜体是我加的,后面解释的作用域链(即使后面新增了全局变量,在函数调用时,也是可以访问到在函数调用前声明的全局变量,因为作用域链对于变量对象仅仅是引用);

2、当函数被调用时,会创建一个执行环境(另一种说法:函数的环境会被推入环境栈中),在这个环境里通过复制创建时的作用域链构建当前环境的作用域链;

3、在当前环境里,创建一个变量对象(因为当前环境是函数,所以直接把活动对象作为变量对象)保存函数环境内创建的局部变量;

4、把这个活动对象添加到作用域链的前端(作用域链中现在多了一个活动对象作为变量对象,这个活动对象的优先级高于全局变量对象);

5、函数访问变量时,从前面的作用域链中的变量对象里找,按照从局部到全局的顺序找,如果找到,返回该变量,找到全局也没有找到,报错;

6、如果函数返回一个非函数的变量或者没有把一个嵌套函数存储在某处的属性里,实际上将会返回变量的值(引用类型则为保存在堆内存中的地址,这个引用类型的值在函数执行完毕后不会销毁);

7、函数执行完毕,该环境被销毁(另一种说法:栈将其环境弹出),该函数的作用域链和局部变量对象销毁,但全局变量对象保存;

8、如果函数返回了一个函数N(匿名函数或函数内声明的函数),或者把这个函数N存储在某处的属性里;这时就会有一个外部引用指向NN就不会被当做垃圾回收,并且N所指向的变量对象也不会被当做垃圾回收(因为函数N在创建时,就创建了作用域链,这个作用域链中有对于他的外部函数变量对象和全局变量对象的引用,所以导致了N和N所指向的变量对象都不会被垃圾回收,尽管外部函数的作用域链会被销毁)。(外部函数的执行环境应该也被销毁?

引申:闭包

备注,上面总结里,第一点和最后一点,在上面的案例没有完全体现出来,实际上在闭包中体现的更明显一点:

1、函数创建时就会创建一个包含全局变量对象和外部函数变量对象的作用域链。

8、如果函数返回了一个函数N(匿名函数或函数内声明的函数),或者把这个函数N存储在某处的属性里;这时就会有一个外部引用指向NN就不会被当做垃圾回收,并且N所指向的变量对象也不会被当做垃圾回收(因为函数N在创建时,就创建了作用域链,这个作用域链中有对于他的外部函数变量对象和全局变量对象的引用,所以导致了N和N所指向的变量对象都不会被垃圾回收)。

1、函数返回了一个函数N

代码:

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。之所以还能够访问这个变量,是因为内部函数的作用域链中包含
createComparisonFunction()的作用域。

这个地方能理解么?

高程解释,就是:

在匿名函数从createComparisonFunction()中被返回后,它的作用域链被初始化为包含
createComparisonFunction()函数的活动对象和全局变量对象。

按上面总结的第1点解释就是:

这个匿名函数创建时(被 return 的时候)就会创建一个包含全局变量对象和外部函数(这里就是createComparisonFunction()函数)变量对象的作用域链。

看图:

高程继续说:

这样,匿名函数就可以访问在 createComparisonFunction()中定义的所有变量。更为重要的是,createComparisonFunction() 函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。

换句话说,当createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁,

当createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,图示:

但它的活动对象仍然会留在内存中;图示:

这就是上面总结的最后1点:

如果函数返回了一个函数N(这里就是匿名函数);这时就会有一个外部引用指向N(赋值给了全局变量compare),N就不会被当做垃圾回收,

并且N所指向的变量对象也不会被当做垃圾回收

因为函数N在创建时,就创建了作用域链,这个作用域链中有对于他的外部函数(这里就是 createComparisonFunction()函数)变量对象和全局变量对象的引用,所以导致了N和N所指向的变量对象都不会被垃圾回收。

并且N所指向的变量对象(这里就是 createComparisonFunction 的活动对象)也不会被当做垃圾回收,图示:

然后针对这种情况,高程给出的方案就是

直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁

结合上面的代码就是:

//解除对匿名函数的引用(以便释放内存)
compareNames = null;

2、把这个函数N存储在某处的属性里(书上没有给出案例,此处自己猜测

代码:

function counter() {
    var n = 0;
    return {
        count: function() {
            return n++;
        },
        reset: function() {
            n = 0;
        }
    };
}

var c = counter(),
    d = counter();
console.log(c.count()); //0
console.log(d.count()); //0:他们互不干扰;
console.log(c.reset()); //reset() 和 count() 共享状态
console.log(c.count()); //0:因为我们重置了c
console.log(d.count()); //1:而没有重置d

这里和上面类似,分析起来也是一样的,因为返回的对象里的两个方法:count 和 reset 的作用域链中都保留着对 counter() 变量对象和全局变量对象的引用,所以 counter() 变量对象不会被垃圾回收,返回的对象也不会被垃圾回收(因为 c,d 还存着对于对象的引用),图示:

如果要让 counter() 变量对象被垃圾回收,需要释放引用,我觉得有两种方式:

c = null;
d = null;

这样返回的对象也会被回收,或者仅仅回收 counter() 变量对象的话:

c.count = null;
c.reset = null;
d.count = null;
d.reset = null;

参考资料:

JavaScript权威指南-第6版-中

JavaScript高级程序设计-第3版

原文地址:https://www.cnblogs.com/xianshenglu/p/8392152.html