Higanbana

深入理解闭包(六)——闭包

终于讲到闭包了,这一路走来不容易。
从前面的博文中我们知道,js的垃圾回收机制会在某个函数的执行上下文生命周期结束后将其回收,释放内存,但是闭包的存在会阻止这一过程。

概念

js高程对闭包的定义是:闭包是指有权访问另一个函数作用域中的变量的函数。 创建闭包的常见方式就是在一个函数内部创建另一个函数,作为返回值或参数传递到函数外部。但是根据我的经验,只要在一个函数内部使用了关键字function,一个闭包就被创建了。

实例

任何书面上的解释都不如一个实例来的有效。

1
2
3
4
5
6
7
8
9
10
var a = 1;
function test() {
var b = 2;
return function () {
var c = 3;
console.log(a+b+c);
}
}
var result=test();
result(); //6

按照我们之前的说法,函数外部是不能访问函数内部的变量的,代码执行到var result=test()这句时调用test函数,执行完毕后应该销毁执行上下文,其中的变量都不能再访问,但是执行result()时却仍然调用了test中的变量,这是因为我们在test函数中返回了一个匿名函数。
test函数中返回的不仅仅是一个函数,还有它的执行上下文环境,将其赋值给全局变量result后,那么result其实就等同于那个返回的匿名函数,当它被调用时可以访问匿名函数作用域链中指向的变量对象(并不是把变量对象复制给了result,只是可以被引用),这个返回的匿名函数就是闭包。
引用阮一峰老师对闭包的解释:闭包本质上是将函数内部和外部连接起来的桥梁

再看一个栗子:

1
2
3
4
5
6
7
8
9
10
function out() {
var a = 1;
var inner=function () {
console.log(a);
}
a++;
return inner;
}
var b = out();
b(); //2

肯定会有人觉得输出的应该是1,想着a++在console.log(a)后面,这是一种常见的错觉,代码创建的位置和执行的顺序是不一样的,这点要谨记。这段代码里首先执行的是调用out函数,out函数执行代码,最后一步返回inner,要知道a++是在return之前执行的,a=2已经保存到了变量对象中,接下来调用b函数(等同于调用inner函数),输出的a自然就是2.

复杂一点的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var fun1,fun2,fun3;
function test() {
var a=10;
fun1=function () {console.log(a);};
fun2=function () {a++;};
fun3=function (x){a=x;};
}
test();
fun2();
fun1(); //11
fun3(5);
fun1(); //5
var fun4=fun1;
test();
fun1(); //10
fun4(); //5

如果上一个栗子你已经理解,那么理解这个栗子前两次输出的11和5也不是什么难事,但后面的两个输出10和5你可能会有点疑惑。

  • 我们第二次调用test函数时一个新的闭包被创建,它能访问到的变量也是重新创建的,跟前面的没有关系,因此再调用fun1时输出10。我们要记住这句话:如果你在一个函数内部声明了另一个函数,那么这个外部函数每次被调用都会产生一个闭包,创建崭新的执行上下文环境。
  • 那么调用fun4怎么又输出了5呢,这是因为var fun4=fun1是在第一次调用test发生的,那么fun4可以访问的变量也是第一次调用test时创建的变量对象,即使在别的地方被调用,它的作用域链也就是可访问的变量是不变的。我们要记住这句话:一个函数可以访问的变量对象要到创建这个函数的执行环境中去找而不是调用这个函数的执行环境。

还有一个非常常见的栗子,那就是闭包对循环的影响:

1
2
3
4
5
6
7
8
9
10
11
12
var arr=[];
function test(){
for (i= 0;i<3;i=i+1){
arr[i]=function(){
console.log(i);
};
}
}
test();
arr[0](); // 3
arr[1](); // 3
arr[2](); // 3

函数运行之后会得到一个函数数组,你本想让每个函数都返回自己的索引值,比如运行arr[0]()时得到0,运行arr[1]()时得到1。但实际上,每个函数都输出3。这是因为闭包只能取得包含函数中任何变量的最后一个值,每个函数的作用域链中都保存着test函数中的变量对象,所以它们引用的是同一个变量i,而i的最后一个值是3,因此每个函数都输出3。我们可以通过创建一个自执行匿名函数来让闭包符合预期:

1
2
3
4
5
6
7
8
9
var arr=[];
function test(){
for (i= 0;i<3;i=i+1){
arr[i]=(function(num){
console.log(num);
})(i);
}
}
test(); //0,1,2

此处用到了自执行匿名函数(function(){···})(),它的写法是,在函数体外面加一对圆括号,形成一个表达式,在圆括号后面再加一个圆括号,里面可传入参数。由于函数参数是按值传递的,所以就可以把变量i的当前值赋值给匿名函数的参数num,而自执行匿名函数可以不用调用,自己执行自己,因此只要检测到参数i就会立即执行并把结果传递给arr数组。这样一来我们就能得到预期的结果了。

缺点

最后一个栗子告诉我们,闭包有时也会给我们的工作带来负面效果。除此之外,还有两个缺点我们需要注意一下。

  1. 内存泄漏: 在ie9之前的版本中,如果闭包的作用域链中保存着一个html元素,那么就意味着该元素将无法销毁。
  2. 占用内存:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,所以我们最好只在绝对必要时再考虑使用闭包。