Fork me on GitHub
余鸢

熟练闭包

对于那些使用不支持闭包特性的编程语言的程序员来说,闭包可能是一个陌生的概念。幸运的是,闭包真没什么可害怕的。

理解闭包

理解闭包只需要学会三个基本的事实。第一个事实:JavaScript允许你引用在当前函数以外定义的变量。

1
2
3
4
5
6
7
8
function makeSandwich() {
var maginIngredient = "peanut butter";
function make(filling) {
return magincIngredient + " and " + filling;
}
return make("jelly");
}
makeSandwich(); //"peanut butter and jelly"

请注意内部的make函数是如何引用定义在外部makeSandwich函数内部的maginIngredient变量的。

第二个事实:即使外部函数已经返回,当前函数仍然可以引用在外部函数所定义的变量。如果这听起来让人难以置信,请记住,JavaScript的函数是第一类(first-class)对象。这意味着你可以返回一个内部函数,并在稍后调用它。

1
2
3
4
5
6
7
8
9
10
11
function sandwich() {
var maginIngredient = "peanut butter";
function make(filling) {
return magincIngredient + " and " + filling;
}
return make;
}
var f = sandwich();
f("jelly"); //"peanut butter and jelly"
f("bananas"); //"peanut butter and bananas"
f("marsh"); //"peanut butter and marsh"

这与第一个列子几乎完全相同。不同的是,不是在外部的sandwich函数中立即调用make("jelly"),而是返回make函数奔三。因此,f的值为内部的make函数,调用f实际上是调用make函数。但即使sandwich函数已经返回,make函数仍然能记住magincIngredient的值。

这是如何工作的?答案是:JavaScript的函数值包含了比调用它们时执行所需要的代码还要多的信息。而且,JavaScript函数值还在内部存储它们可能会引用的定义在其封闭作用域的变量。那些在其所涵盖的作用域内跟踪变量的函数被称为闭包。make函数就是一个闭包,其代码引用了两个外部变量:maginIngredientfilling。每当make函数被调用时,其代码都会引用到这两个变量,因为该闭包存储了这两个变量。

函数可以引用在其作用域内的任何变量,包括参数和外部函数变量。我们可以利用这点来编写更加通用的sandwich函数。

1
2
3
4
5
6
7
8
9
10
11
12
function sandwich(maginIngredient) {
function make(filling) {
return magincIngredient + " and " + filling;
}
return make;
}
var hamAnd = sandwich("ham");
hamAnd("cheese"); //"ham butter and cheese"
hamAnd("bananas"); //"ham butter and bananas"
var turkeyAnd = sandwich("turkey");
turkeyAnd("Swiss"); //"turkey butter and Swiss"
turkeyAnd("Provolone") //"turkey butter and Provolone"

该函数创建了hamAnd和turkeyAnd两个完全不同的函数。尽管它们都是由相同的mak函数定义的,但是它们是两个截然不同的对象。第一个函数的magincIngredient值为“ham”,而第二个函数的magincIngredient值为“turkeyAnd”。

闭包是JavaScript最优雅、最有表现力的特性之一,也是许多惯用法的核心。JavaScript甚至还提供了一种更为方便的构建闭包的字面量语法——函数表达式。

1
2
3
4
5
function sandwich(maginIngredient) {
return function(filling) {
return magincIngredient + " and " + filling;
}
}

请注意,该函数表达式是匿名的。由于我们只需要其能产生一个新的函数值,而不打算在局部调用它,因此根本没有必要给该函数命名。函数表达式也可以有名称。

学习闭包的第三个也是最后一个事实:闭包可以更新外部变量的值。实际上,闭包存储的是外部变量的引用,而不是它们的值的副本。因此,对于任何具有访问这些外部变量的闭包,都可以进行更新。一个简单的惯用法box对象说明了这一切。它存储了一个可读写的内部值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function box() {
var val = undefined;
return {
set: function (newVal) {
val = newVal;
},
get: function () {
return val;
},
type: function () {
return typeof val;
}
}
}
var b = box();
b.type(); //undefined
b.set(98);
b.get(); //98
b.type(); //"number"

该例子产生了一个包含三个闭包的对象。这三个闭包是set,get,type属性。它们共同访问val变量。set闭包更新了val的值,随后调用get和type查看更新的结果。

提示:

  • 函数可以引用定义在其外部作用域的变量。
  • 闭包比创建它们的函数有更长的生命周期。
  • 闭包在内部存储其外部变量的引用,并能读写这些变量。