理解闭包的最重要前提
理解作用域链,如何创建作用域链和作用域链中有什么细节。
闭包示例
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;
}
}
原因
内部函数被返回了,它访问了外部函数中的变量propertyName。即使这个内部函数被返回了,而且是在其他地方被调用了,但它仍然你可以访问变量propertyName。之所以还能够访问这个变量,是因为内部函数的作用域包含createComparisonFunction()的作用域
函数被创建发生了什么?
当某个函数被调用时,会创建一个执行环境execution context,以及相应的作用域链scope chain。然后,使用arguments和其他命名参数的值来初始化函数的activation object 活动对象。
但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象是第三位,。。直到全局执行环境
function compare(value1,value2){
if(value1<value2)
return -1;
else if(value1>value2)
return 1;
else
return 0;
}
var result=compare(5,10);
这个compare函数就会创建一个包含argument,value1,value2的活动对象。 全局执行环境的变量对象 此时处于第二位。如图:
简单回顾
变量对象
后台的每个执行环境都有一个表示变量的对象-变量对象。 对于compare()这样的普通函数的变量对象,只在函数执行的过程中存在。
作用域链
创建这个函数的时候,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[scope]]属性中。
本质:作用域链本质只是一个指向变量对象的指针列表,它只引用,不实际包含变量对象!
执行环境
当调用compare()函数的时候,会为函数创建一个执行环境,然后通过复制函数的[[scope]]属性中的对象构建起执行环境的作用域链。
此后又有一个活动对象(在此作为变量对象使用)被创建并推入执行环境作用域链的前端。
关于闭包和销毁
了解了上述的概念之后,我们要知道的是。 无论什么时候在函数中访问一个变量就会从作用域链中搜索具有相应名字的变量。 一般来说函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(确切说是全局执行环境的变量对象) 但是闭包不一样,它不会在函数执行结束后被销毁!
原理
在一个外部函数中定义的内部函数,内部函数的作用域链中会包含外部函数的活动对象和全局变量对象。 这样内部函数(内部匿名函数)就可以访问在外部函数中定义的所有变量。当这个外部函数执行完毕后,它的活动对象不会被销毁,因为内部匿名函数的作用域链仍然在引用这个外部函数执行环境中的活动对象。
重申外部函数执行完毕后,它的执行环境的作用域链会被销毁,但是它的活动对象仍然在内存中! 直到内部匿名函数被销毁后才会被销毁!
var compareNames=createComparisonFunction("name");
var result=compareNames({name:"Nico"},{name:"Greg"});
//解除对匿名函数的引用
compareNames=null;
V8引擎会尝试回收被闭包占用的内存,但还是要慎用。
闭包与变量
最经典的闭包问题:
function createFunction(){
var result=[];
for(var i=0;i<10;i++){
result[i]=function(){
return i;
};
}
return result;
}
这个函数中,for循环中的i实质是外部函数中的变量,它是外部函数createFunction活动对象的一个属性。 然后内部这个匿名函数,它产生了一个闭包,这个闭包保存了整个外部函数的变量对象。实际上这里的循环,我们创建了多个闭包(作用域),但是它们都保存了createFunction的活动对象,所以引用的是同一个变量。
解决方法
解决这个问题的方法是创建另一个匿名函数强制让闭包行为符合预期:
function createFunction(){
var result=[];
for(var i=0;i<10;i++){
result[i]=function(num){
return function(){
return num;
};
}(i); //IIFE
}
return result;
}
我们不把闭包直接赋值给数组,而是再定义一个匿名函数,并将立即执行该匿名函数的结果赋给数组。 传入参数num给这个匿名函数,因为函数参数按值传递,所以就会将变量i复制给num,所以现在不再担心因为闭包保存着外部函数的活动对象而得到一个不符合预期的值。
而最内部的匿名函数拥有它外部匿名函数的活动对象(即这是一个闭包),这个num值在每次循环是会改变的。