一、闭包的定义
函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为“闭包”。——《JavaScript权威指南》
二、为什么使用闭包
首先让我们来看一下全局变量与局部变量的区别:
全局变量:可以反复使用,且共享使用;但是可能随时在任意位置被篡改(全局污染)。
局部变量:不可反复使用;但方法调用完毕自动释放。
所以我们想要一种即不会被污染,又可以反复使用的局部变量时,就可以使用闭包。
三、如何使用闭包
使用闭包需要三步:
1.定义外层函数
特点:定义了受保护的局部变量,返回了一个专门操作局部变量的内部函数对象。
2.调用外层函数,获得返回的内部函数对象。
3.使用获得的内部函数对象,操作受保护的局部变量。(这是操作受保护局部变量的唯一途径)
只看定义不能让人很好的理解,下面看一下示例代码:
//定义外层函数保护局部变量
function fun(){
var n=0;
return function(){
return ++n;
}
}
//获得返回的内部函数对象
var f1=fun();//实际上,var f1=function(){return ++n;}
//使用内部函数对象操作受保护的变量
console.log(f1());//1
console.log(f1());//2
n=1;//在此处尝试修改受保护的变量,并没有影响输出的结果
console.log(f1());//3
四、如何判断闭包
1.必须有内外层函数嵌套;
2.内层函数必须使用外层函数的局部变量;
3.外层函数将内层函数返回到外部,可在外部调用。
注:可以参考上面的示例代码,对应判断的条件。
五、判断闭包的结果
1.外层函数调用了几次,就有几个受保护的局部变量副本;
2.同一次外层函数调用返回的内部函数对象,操作同一个变量。
上面的例子是一个最简单的闭包,下面来看一些有点意思的:
function outer(){
for(var i=0,arr=[];i<3;i++){//i受保护的变量
arr[i]=function(){return i};
}//i变成了3
return arr;
}
var funs=outer(); //外层函数调用一次,只有1个i
console.log(funs[0]()); //3
console.log(funs[1]()); //3
console.log(funs[2]()); //3
从表面上看,似乎每个函数都应该返回自己的索引,即0位置的函数返回0,1位置的函数返回1,然而实际并不是这样;实际上,每个函数都返回3。
注:作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取到包含函数中任何变量的最后一个值。——《JavaScript高级程序设计》
根据判断闭包结果的条件,上述代码只调用了一次外层函数outer(),所以只有一个受保护的变量,因为每个函数的作用域链中都保存着outer()函数的活动对象,所有它们引用的都是同一个变量i;当outer()函数返回后,变量i的值是3,此时每个函数都引用着保存变量i的同一个变量对象,所以每个函数内部i的值都是3。
当然如果我们要实现每个函数都返回自己的索引也是有办法的,看下面的代码:
function outer(){
for(var i=0,arr=[];i<3;i++){//i受保护的变量
arr[i]=function(num){
return function(){
return num;
}
}(i);
}//i变成了3
return arr;
}
var funs=outer();
console.log(funs[0]()); //0
console.log(funs[1]()); //1
console.log(funs[2]()); //2
这里我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋值给数组。这里的匿名函数有一个参数num,也就是最终的函数要返回的值。在调用每个匿名函数时,我们传入了变量i。由于函数参数是按值传递的,所以就会将变量i的当前值复制给参数num。而在这个匿名函数内部,又创建并返回了一个访问num的闭包。这样一来,arr数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值了。——《JavaScript高级程序设计》
六、闭包中的this对象
看下边两个代码:
//例6-1
var name="global";
var obj={
name:"local",
getNameFun:function(){
return function(){
return this.name;
};
}
};
console.log(obj.getNameFun()());//"global"
//例6-2
var name="global";
var obj={
name:"local",
getNameFun:function(){
var self=this;
return function(){
return self.name;
};
}
};
console.log(obj.getNameFun()());//"local"
内部函数在搜索this变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。当然这并不是没有解决办法,如例6-2中,把外部作用域中的this对象保存在一个闭包能够访问的变量里,就可以让闭包访问该对象了。
个人觉得下面的代码是比较难的(有兴趣的可以自己研究一下):
var o=10;
var foo={
o:9,
bar:function(){
return this.o;
}
}
console.log(foo.bar());//9
var bar=foo.bar;
console.log(bar());//10
console.log((foo.bar=foo.bar)());//10
console.log((foo.bar,foo.bar)());//10
七、闭包的问题
1.由于闭包在内存中造成了循环引用,而当函数的执行环境被释放时,函数的活动对象还被引用无法释放,所以比普通函数占用更多的内存空间;
2.当DOM对象和JavaScript对象之间存在循环引用时需要格外小心,在某些浏览器(主要是IE)下会造成内存泄漏。
简单来说就是,如果闭包的作用域链中保存着一个HTML元素,那么久意味着该元素将无法被销毁。
以下均出之《JavaScript高级程序设计》:
//例7-1
function assignHandler(){
var element=document.getElementById("someElement");
element.onclick=function(){
alert(element.id);
}
}
以上代码创建了一个作为element元素时间处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就导致无法减少element的引用数。只要匿名函数存在,element的引用数至少也是1,因此它所占用的内存就永远不会被收回。不过可以稍微改写一下代码来解决,如例7-2。
//例7-2
function assignHandler(){
var element=document.getElementById("someElement");
var id=element.id;
element.onclick=function(){
alert(id);
}
element=null;
}
注:仅仅做到这一步,还是不能解决内存泄漏的问题。
必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着element。即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把element变量设置有null。这样就能够解除对DOM对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。