理解闭包

什么是闭包?

闭包(Closure),是javascript中一个十分重要的概念。我在学习其他语言时从未听说这个概念,也是在学习javascript中第一次接触到闭包。下面两个例子就是对闭包概念的一个很好的入门。

1
2
3
4
5
6
7
8
function outer() {
var name = 'Jason';
function inner() {
alert(name);
}
inner();
}
outer();

这段代码的输出结果是弹窗输出字符串Jason。简单地来分析一下这段代码,外面的outer()函数创建了一个局部变量name,与一个函数inner()。里面的inner()函数中使用了父函数域内定义的变量name。父函数outer()的第6行直接调用了inner()。在第8行我们在父函数外部直接调用父函数outer(),于是inner()也得到了调用。

闭包最浅显的理解就是函数嵌套函数的结构,在这段代码中inner()这个子函数就可以被理解成闭包,inner()虽然自己没有定义任何一个变量,但是他却拥有父函数定义的变量的使用权。

再来看另外一个例子

1
2
3
4
5
6
7
8
9
function outer() {
var name = 'Jason';
function inner() {
alert(name);
}
return inner;
}
var exec = outer();
exec();

这个例子与上一个例子的输出结果完全相同。细细观察这个新的例子就会发现,在这个例子中,outer()函数中并没有对inner()函数直接进行调用,而是在第6行返回了这个函数。在第8行中,外面定义的变量exec通过调用父函数来获得子函数inner(),第九行才正真调用了这个inner()函数。这时候一个令人困惑的地方就出现了,name明明是一个局部变量他是如何在第8outer()执行完后依然存活了下来。这就是javascript厉害的地方了,其实任何的闭包其实不光光是一个嵌套函数,它还包含了一个词法环境(lexical environment),在这个词法环境中包含了所有这个嵌套函数定义时所能获取的变量。所以这里有一点需要注意的就是,任何在闭包内不需要的变量请不要让闭包能够拥有这些变量的使用权,因为这样会使得相应的词法环境储存多余的变量,引起不必要的内存泄漏。

闭包的应用

闭包的应用其实从上面的第二个例子中已经可以瞥见一二了。闭包可以通过词法环境让你的数据与函数结合在一起,实现OOP最基本的函数与数据相结合的理念。更关键的是,通过闭包你可以模拟C++或者Java在类概念中定义的私有(private)与公有(public)这两个概念。下面我就用一个Mozilla提供的js计数器的案例来解释如何用闭包来进行模拟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
};
})();
console.log(counter.value()); // 控制台输出 0
counter.increment();
console.log(counter.value()); // 控制台输出 1
counter.decrement();
console.log(counter.value()); // 控制台输出 0

上面这段代码就是一个在javascript中使用闭包来进行模块化开发的典型。我们从外到内来剖析这段代码。最外面我们使用了一个立即执行函数(注意17行最后的双括号调用本函数),由这个立即执行函数来给第一行定义的变量counter赋值。在这个立即执行函数的内部,我们可以看到这个函数定义了一个叫privateCounter的变量,定义了一个叫changeBy的函数。看到第4changBy函数中的内容,changBy唯一的作用就是增加或减少privateCounter的值。接下来看到此函数中的重头戏,这个return在一个返回对象中封装了三个不同的函数对象。这三个不同的对象分别是 incrementdecrementvalue。接下来我们来分别分析这三个函数对象的作用。

increment

如它的名字所示的那样increment在第八行调用了changeBy这个在其词法环境中定义的函数,为privateCounter这个变量增加了1。

decrement

类同于increment,此函数也调用了changByprivateCounter减少了1。

value

此函数返回privateCounter的值。

分析

当理解了这三个函数对象的作用后,我们很容易得出18行控制台输出的值为0,20行在加1后输出1,22行在减1后再次输出0。可以看出counter在这里完全模拟了一个Java或者C++中的类。privateCounter是一个典型的私有变量,changeBy是一个私有函数,而incrementdecrementvalue则是三个公有函数。我们利用三个闭包的函数对象留住了私有变量与私有函数,使得它们不被垃圾回收器给回收,从而在javascript中实现了公有与私有的特性。

总结

闭包是javascript中一个十分有趣的特性,利用闭包我们能在javascript中实现面向对象开发的一些基本特性。理解闭包在前端的模块化开发中是十分必要的,但切记闭包中的词法环境这个特性注定了闭包是一个十分消耗资源的手段,如能通过不闭包实现的功能,请不要用闭包来实现。