1
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
|
---
title: 闭包
slug: Web/JavaScript/Closures
tags:
- ES5
- JavaScript
- 中级的
- 手册
- 指南
- 闭包
translation_of: Web/JavaScript/Closures
---
<div>{{jsSidebar("Intermediate")}}</div>
<p>一个函数和对其周围状态(<strong>lexical environment,词法环境</strong>)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是<strong>闭包</strong>(<strong>closure</strong>)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。</p>
<h2 id="词法作用域"><strong>词法作用域</strong></h2>
<p>请看下面的代码:</p>
<pre class="brush: js">function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变量
function displayName() { // displayName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量
}
displayName();
}
init();</pre>
<p><code>init()</code> 创建了一个局部变量 <code>name</code> 和一个名为 <code>displayName()</code> 的函数。<code>displayName()</code> 是定义在 <code>init()</code> 里的内部函数,并且仅在 <code>init()</code> 函数体内可用。请注意,<code>displayName()</code> 没有自己的局部变量。然而,因为它可以访问到外部函数的变量,所以 <code>displayName()</code> 可以使用父函数 <code>init()</code> 中声明的变量 <code>name</code> 。</p>
<p>使用<a href="http://jsfiddle.net/xAFs9/3/" title="http://jsfiddle.net/xAFs9/">这个 JSFiddle 链接</a>运行该代码后发现, <code>displayName()</code> 函数内的 <code>alert()</code> 语句成功显示出了变量 <code>name</code> 的值(该变量在其父函数中声明)。这个<em>词法作用域</em>的例子描述了分析器如何在函数嵌套的情况下解析变量名。词法(lexical)一词指的是,词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。</p>
<h2 id="闭包"><strong>闭包</strong></h2>
<p>现在来考虑以下例子 :</p>
<pre class="brush: js">function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();</pre>
<p>运行这段代码的效果和之前 <code>init()</code> 函数的示例完全一样。其中不同的地方(也是有意思的地方)在于内部函数 <code>displayName()</code> <em>在执行前</em>,从外部函数返回。</p>
<p>第一眼看上去,也许不能直观地看出这段代码能够正常运行。在一些编程语言中,一个函数中的局部变量仅存在于此函数的执行期间。一旦 <code>makeFunc()</code> 执行完毕,你可能会认为 <code>name</code> 变量将不能再被访问。然而,因为代码仍按预期运行,所以在 JavaScript 中情况显然与此不同。</p>
<p>原因在于,JavaScript中的函数会形成了闭包。 <em>闭包</em>是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。在本例子中,<code>myFunc</code> 是执行 <code>makeFunc</code> 时创建的 <code>displayName</code> 函数实例的引用。<code>displayName</code> 的实例维持了一个对它的词法环境(变量 <code>name</code> 存在于其中)的引用。因此,当 <code>myFunc</code> 被调用时,变量 <code>name</code> 仍然可用,其值 <code>Mozilla</code> 就被传递到<code>alert</code>中。</p>
<p>下面是一个更有意思的示例 — 一个 <code>makeAdder</code> 函数:</p>
<pre class="brush: js">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
</pre>
<p>在这个示例中,我们定义了 <code>makeAdder(x)</code> 函数,它接受一个参数 <code>x</code> ,并返回一个新的函数。返回的函数接受一个参数 <code>y</code>,并返回<code>x+y</code>的值。</p>
<p>从本质上讲,<code>makeAdder</code> 是一个函数工厂 — 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和,另一个和 10 求和。</p>
<p><code>add5</code> 和 <code>add10</code> 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 <code>add5</code> 的环境中,<code>x</code> 为 5。而在 <code>add10</code> 中,<code>x</code> 则为 10。</p>
<h2 id="Practical_closures">实用的闭包</h2>
<p>闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。</p>
<p>因此,通常你使用只有一个方法的对象的地方,都可以使用闭包。</p>
<p>在 Web 中,你想要这样做的情况特别常见。大部分我们所写的 JavaScript 代码都是基于事件的 — 定义某种行为,然后将其添加到用户触发的事件之上(比如点击或者按键)。我们的代码通常作为回调:为响应事件而执行的函数。</p>
<p>假如,我们想在页面上添加一些可以调整字号的按钮。一种方法是以像素为单位指定 <code>body</code> 元素的 <code>font-size</code>,然后通过相对的 <code>em</code> 单位设置页面中其它元素(例如<code>header</code>)的字号:</p>
<pre class="brush: css">body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
</pre>
<p>我们的文本尺寸调整按钮可以修改 <code>body</code> 元素的 <code>font-size</code> 属性,由于我们使用相对单位,页面中的其它元素也会相应地调整。</p>
<p>以下是 JavaScript:</p>
<pre class="brush: js">function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
</pre>
<p><code>size12</code>,<code>size14</code> 和 <code>size16</code> 三个函数将分别把 <code>body</code> 文本调整为 12,14,16 像素。我们可以将它们分别添加到按钮的点击事件上。如下所示:</p>
<pre class="brush: js">document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
</pre>
<pre class="brush: html"><a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>
</pre>
<p>{{JSFiddleEmbed("https://jsfiddle.net/cubr4hs0/","","200")}}</p>
<h2 id="Emulating_private_methods_with_closures">用闭包模拟私有方法</h2>
<p>编程语言中,比如 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。</p>
<p>而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。</p>
<p>下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为 <a class="external" href="http://www.google.com/search?q=javascript+module+pattern" title="http://www.google.com/search?q=javascript+module+pattern">模块模式(module pattern):</a></p>
<pre class="brush: js">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()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
</pre>
<p>在之前的示例中,每个闭包都有它自己的词法环境;而这次我们只创建了一个词法环境,为三个函数所共享:<code>Counter.increment,</code><code>Counter.decrement</code> 和 <code>Counter.value</code>。</p>
<p>该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 <code>privateCounter</code> 的变量和名为 <code>changeBy</code> 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。</p>
<p>这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 <code>privateCounter</code> 变量和 <code>changeBy</code> 函数。</p>
<div class="note">
<p>你应该注意到我们定义了一个匿名函数,用于创建一个计数器。我们立即执行了这个匿名函数,并将他的值赋给了变量<code>Counter</code>。我们可以把这个函数储存在另外一个变量<code>makeCounter</code>中,并用他来创建多个计数器。</p>
</div>
<pre class="brush: js">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()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
</pre>
<p>请注意两个计数器 <code>Counter1</code> 和 <code>Counter2</code> 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 <code>privateCounter</code> 。</p>
<p>每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。</p>
<div class="note">
<p>以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。</p>
</div>
<h2 id="Creating_closures_in_loops_A_common_mistake">在循环中创建闭包:一个常见错误</h2>
<p>在 ECMAScript 2015 引入 <a href="/en-US/docs/JavaScript/Reference/Statements/let" title="let"><code>let</code> 关键字</a> 之前,在循环中有一个常见的闭包创建问题。参考下面的示例:</p>
<pre class="brush: html"><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>
</pre>
<pre class="brush: js">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();
</pre>
<p>{{JSFiddleEmbed("https://jsfiddle.net/51brm6ps/", "", 200)}}</p>
<p>数组 <code>helpText</code> 中定义了三个有用的提示信息,每一个都关联于对应的文档中的<code>input</code> 的 ID。通过循环这三项定义,依次为相应<code>input</code>添加了一个 <code>onfocus</code> 事件处理函数,以便显示帮助信息。</p>
<p>运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个<code>input</code>上,显示的都是关于年龄的信息。</p>
<p>原因是赋值给 <code>onfocus</code> 的是闭包。这些闭包是由他们的函数定义和在 <code>setupHelp</code> 作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。这是因为变量<code>item</code>使用var进行声明,由于变量提升,所以具有函数作用域。当<code>onfocus</code>的回调执行时,<code>item.help</code>的值被决定。由于循环在事件触发之前早已执行完毕,变量对象<code>item</code>(被三个闭包所共享)已经指向了<code>helpText</code>的最后一项。</p>
<p>解决这个问题的一种方案是使用更多的闭包:特别是使用前面所述的函数工厂:</p>
<pre class="brush: js">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();
</pre>
<p>{{JSFiddleEmbed("https://jsfiddle.net/v7gjv/12185/", "", 300)}}</p>
<p>这段代码可以如我们所期望的那样工作。所有的回调不再共享同一个环境, <code>makeHelpCallback</code> 函数为每一个回调创建一个新的词法环境。在这些环境中,<code>help</code> 指向 <code>helpText</code> 数组中对应的字符串。</p>
<p>另一种方法使用了匿名闭包:</p>
<pre class="brush: js">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();</pre>
<p>如果不想使用过多的闭包,你可以用ES2015引入的let关键词:</p>
<pre class="brush: js">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();</pre>
<p>这个例子使用<code>let</code>而不是<code>var</code>,因此每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。</p>
<p>另一个可选方案是使用 <code>forEach()</code>来遍历<code>helpText</code>数组并给每一个<code><a href="/zh-CN/docs/Web/HTML/Element/p" title="The HTML <p> element represents a paragraph."><p></a></code>添加一个监听器,如下所示:</p>
<pre class="brush: js">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();</pre>
<h2 id="Performance_considerations">性能考量</h2>
<p>如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。</p>
<p>例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。</p>
<p>考虑以下示例:</p>
<pre class="brush: js">function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
</pre>
<p>在上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:</p>
<pre class="brush: js">function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype = {
getName: function() {
return this.name;
},
getMessage: function() {
return this.message;
}
};
</pre>
<p>但我们不建议重新定义原型。可改成如下例子:</p>
<pre class="brush: js">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;
};
</pre>
<p>在前面的两个示例中,继承的原型可以为所有对象共享,不必在每一次创建对象时定义方法。参见 <a href="/zh-CN/docs/JavaScript/Guide/Details_of_the_Object_Model" title="en-US/docs/JavaScript/Guide/Details of the Object Model">对象模型的细节</a> 一章可以了解更为详细的信息。</p>
|