闭包
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包
(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
# 词法作用域
请看下面的代码:
function init(){
var name = "Mozilla";//name是一个被init创建的局部变量
function displayName(){ //displayName()是内部函数,一个闭包
alert(name); //使用了父函数中声明的变量
}
displayName()
}
init()
2
3
4
5
6
7
8
init()
创建了一个局部变量name
和一个名为displayName()
的函数。displayName()
是定义在init()
里的内部函数,并且仅在init()
函数体内可用。
请注意,displayName()
没有自己的局部变量。然而,因为它可以访问到外包的变量,所以displayName()
可以使用父函数init()
中声明的变量name
。
这个词法作用域
的例子描述了分析器如何在函数嵌套的情况下解析变量名。词法
一词指的是,词法作用域
根据源代码中声明变量的位置
来确定该变量在何处可用。嵌套函数可访问声明于他们外部作用域的变量
# 闭包
现在来考虑以下例子:
function makeFunc(){
var name = "Mozilla";
funciton displayName(){
alert(name);
}
return displayName;
}
var myFunc = makeFunc()
myFunc()
2
3
4
5
6
7
8
9
10
运行这段代码的效果和之前init()
函数的示例完全一样。其中不同的地方(也是有意思的地方)在于内部函数displayName()
在执行前,从外部函数返回。
原因在于,JavaScript中的函数会形成闭包。闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。在本例中,myFunc
是执行makeFunc
时创建的displayName
函数实例的引用。displayName
的实例维持了一个对它的词法环境(变量name
存在于其中)的引用。因此,当myFunc
被调用时,变量name
仍然可用。
下面是一个更有意思的示例,makeAdder
函数:
function makeAdder(x){
return function(y){
return x + y;
}
}
var add5 = makeAdder(5)
var add10 = makeAdder(10)
console.log(add5(2)) // 7
console.log(add10(2)) // 12
2
3
4
5
6
7
8
9
10
11
在这个实例中,我们定义了makeAdder(x)
函数,它接受一个参数x
,并返回一个新的函数。返回的函数接受一个参数y
,并返回x+y
的值。
从本质上讲,makeAdder
是一个工厂函数——他创建了将制定的值和它的参数相加求和的函数。
add5
和add10
都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在add5
的环境中,x
为5。而在add10
中,x
则为10。
# 实用的闭包
闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或多个方法相关联。
因此,通常你使用只有一个方法的对象的地方,都可以使用闭包。
在Web中,你想要这样做的情况特别常见。大部分我们所写的JavaScript代码都是基于事件的,定义某种行为,然后将其添加到用户触发的事件之上(点击或者按键)。我们的代码通常作为回调:为响应事件而执行的函数。
假如,我们想在页面上添加一些可以调整字号的按钮。一种方法是以像素为单位指定body
元素的font-size
,然后通过相对的em
单位设置页面中其它元素(例如header
)的字号:
body {
font-family:Helvetica,Arial,sans-serif;
font-size: 12px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
2
3
4
5
6
7
8
9
10
11
12
我们的文本尺寸调整按钮可以修改body
元素的font-size
属性,由于我们使用相对单位,页面中的其它元素也会相应地调整。
以下是JavaScript:
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
}
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
2
3
4
5
6
7
8
9
size12
,size14
和size16
三个函数分别把body
文本调整为12,4,16像素。我们可以将他们分别添加到按钮的点击事件上。如下图所示:
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
2
3
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>
2
3
# 闭包模拟私有方法
编程语言中,比如Java,是支持将方法声明为私有的,即它们只能被同一个类中的其他方法所调用。
而JavaScript没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问,还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
下面的实例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为模块模式
(module pattern):
var Counter = (function() {
var privateCounter = 0;
fuction changeBy(val) {
privateCounter += val;
}
return {
increment:function() {
changeBy(1);
},
decrement:funciton() {
changeBy(-1);
},
value:function(){
return privateCounter;
}
}
})();
console.log(Counter.value()); // 0
Counter.increment();
Counter.increment();
console.log(Counter.value()); // 2
Counter.decrement();
console.log(Counter.value()); // 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在之前的示例中,每个闭包都有它自己的词法环境;而这次我们只创建了一个词法环境,为三个函数所共享:Counter.increment
,Counter.decrement
和Counter.value
。
该共享环境创建于一个立即执行的匿名函数体内。这个环境包含两个私有项:名为privatecCounter
的变量和名为changeBy
的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。
这三个公共函数是共享同一个环境的闭包。多亏JavaScript的词法作用域,它们都可以访问privateCounter
变量和changeBy
函数。
注意
你应该注意到我们定义了一个匿名函数,用于创建一个计数器。我们立即执行了这个匿名函数,并将他的值赋给了变量Counter
。我们可以把这个函数储存在另一个变量makeCounter
中,并用他来创建多个计数器。
var makeCounter = function(){
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
}
var Counter1 = makeCounter()
var Counter2 = makeCounter()
console.log(Counter1.value()) // 0
Counter1.increment()
Counter1.increment()
console.log(Counter1.value()) // 2
Counter1.decrement()
console.log(Counter1.value()) // 1
console.log(Counter2.value()) // 0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
请注意两个计数器Counter1
和Counter2
是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量privateCounter
。
每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。
提示
以这种方式使用闭包,提供了许多与面向对象编程相关的好处,特别是数据隐藏和封装。
# 在循环中创建闭包:一个常见错误
在ECMAScript 2015 引入 let
关键字之前,在循环中有一个常见的闭包创建问题:
Helpful notes will appear here
E-mail:
Name:
Age:
<p id="help">Helpful notes will appear here</p>
<p>E-mail:<input type="text" id="email" name="email"></p>
<p>Name:<input type="text" id="name" name="name"></p>
<p>Age:<input type="text" id="age" name="age"></p>
2
3
4
function showHelp(help){
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id':'email','help':'Your e-mail address'},
{'id':'name','help':'Your full name'},
{'id':'age','help':'Your age (you must be over 16)'}
]
for(var i = 0;i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function(){
showHelp(item.help)
}
}
}
setupHelp()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
数组helpText
中定义了三个有用的提示信息,每一个都关联于对应的文档中的input
的ID。通过循环这三项定义,依次为相应input
添加了一个onfocus
事件处理函数,以便显示帮助信息。
运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个input
上,显示的都是关于年龄的信息。
原因是赋值给onfocus
的是闭包。这些闭包是由他们的函数定义和在setupHelp
作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item
。这是因为变量item
使用var
进行声明,由于变量提升,所以具有函数作用域。当onfocus
的回调执行时,item.help
的值被决定。由于循环在事件触发之前早已执行完毕,变量对象item
(被三个闭包所共享)已指向helpText的最后一项。
解决这个问题的一种方案是使用更多的闭包:特别是使用前面所述的函数工厂:
Helpful notes will appear here
E-mail:
Name:
Age:
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function makeHelpCallback(help) {
return function() {
showHelp(help)
}
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for(var i = 0;i<helpText.length;i++){
var item = helpText[i]
document.getElementById(item.id).onfocus = makeHelpCallback(item.help)
}
}
setupHelp();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这段代码可以如我们所期望的那样工作。所有回调不再共享一个环境,makeHelpCallback
函数为每一个回调创建一个新的词法环境。在这个环境中,help
指向helpText
数组中对应的字符串。
另一种方法使用了匿名闭包:
function showHelp(help) {
document.getElementById('help').innerHTML = help
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for(var i = 0;i < helpText.length;i++){
(function() {
var item = helpText[i];
document.getElementById(item.id).onfocus = function(){
showHelp(item.help)
}
})() //马上把当前循环项的item与事件回调相关联起来
}
}
setupHelp()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
如果不想使用过多的闭包,你可以使用ES2015引入的let
关键词:
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
let item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这个例子使用let
而不是var
,因此每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。
另一个可选方案是使用forEach()
来遍历helpText
数组并给每一个<p>
添加一个监听器,
如下所示:
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
helpText.forEach(function(text) {
document.getElementById(text.id).onfocus = function() {
showHelp(text.help);
}
});
}
setupHelp();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 性能考量
如果不是某些特定任务需要使用闭包,在其他函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。
考虑以下示例:
function MyObject(name, message) {
this.name = name.toString()
this.message = message.toString()
this.getName = function(){
return this.name
}
this.getMessage = function() {
return this.message
}
}
2
3
4
5
6
7
8
9
10
11
在上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包,修改成如下:
function MyObject(name,message) {
this.name = name.toString()
this.message = message.toString()
}
MyObject.prototype = {
getName: function() {
return this.name
},
getMessage: function() {
return this.message
}
}
2
3
4
5
6
7
8
9
10
11
12
13
但我们不建议重新定义原型。可以改成如下例子:
function MyObject(name, message) {
this.name = name.toString()
this.message = message.toString()
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
2
3
4
5
6
7
8
9
10
11