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
|
---
title: 閉包
slug: Web/JavaScript/Closures
translation_of: Web/JavaScript/Closures
---
<div>{{jsSidebar("Intermediate")}}</div>
<p class="summary">閉包(Closure)是函式以及該函式被宣告時所在的作用域環境(lexical environment)的組合。</p>
<h2 id="語法作用域(Lexical_scoping)">語法作用域(Lexical scoping)</h2>
<p>思考這個例子:</p>
<div>
<pre class="brush: js">function init() {
var name = "Mozilla"; // name 是個由 init 建立的局部變數
function displayName() { // displayName() 是內部函式,一個閉包
alert(name); // 使用了父函式宣告的變數
}
displayName();
}
init();</pre>
</div>
<p><code>init()</code> 建立了局部變數 <code>name</code> 與 <code>displayName()</code> 函式。<code>displayName()</code> 是個在 <code>init()</code> 內定義的內部函式,且只在該函式內做動。<code>displayName()</code> 自己並沒有局部變數,不過它可以訪問外面函式的變數、因而能取用在父函式宣告的變數 <code>name</code>。</p>
<p>{{JSFiddleEmbed("https://jsfiddle.net/78dg25ax/", "js,result", 250)}}</p>
<p><a href="http://jsfiddle.net/xAFs9/3/" title="http://jsfiddle.net/xAFs9/">運行</a>這個程式碼並注意 <code>displayName()</code> 裡面的 <code>alert()</code> 宣告,它能顯示位於上一層的 <code>name</code> 變數。這實例在描述<em>語法作用域</em>碰上巢狀函式時,解析器(parser)會如何解讀(resolve)變數。「作用域」一詞,指的正是作用域環境在程式碼指定變數時,使用 location 來決定該變數用在哪裡的事情。巢狀函式的內部函式,能訪問在該函式作用域之外的變數。</p>
<h2 id="閉包">閉包</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> 有相同結果:字串 Mozilla 會以 JavaScript alert 提示顯示出來。但箇中不同、且頗具趣味處,乃內部函式 <code>displayName()</code> 竟在外部函式執行前,從其回傳。</p>
<p>乍看之下,這段程式碼看來不大直覺:在某些程式語言,函式內的局部變數,只會在函式的執行期間存在。當 <code>makeFunc()</code> 執行完,你可能會預期 name 變數再也無法使用。但這段能照舊運行的程式碼表明了,在 JavaScript 並不是這樣做。</p>
<p>箇中理由和 JavaScript 函式的閉包有關。<em>閉包</em>為函式的組合、還有該宣告函式的作用域環境。這個環境包含閉包建立時,所有位於該作用域的區域變數。在這個例子中,<code>myFunc</code> 是 <code>displayName</code> 在 <code>makeFunc</code> 運行時所建立的實例(instance)參照。<code>displayName</code> 的實例保有了其作用域環境的參照,作用域裡則內含 <code>name</code> 變數。因此,在調用 <code>myFunc</code> 時,<code>name</code> 變數被保存、並能作它用。「Mozilla」一詞也因此傳給了 <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>x</code> 並回傳新函式的函式 <code>makeAdder(x)</code>。該新函式又帶有 <code>y</code> 參數並回傳了 <code>x</code> 與 <code>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="實用的閉包">實用的閉包</h2>
<p>閉包實用之處,在於能讓你把一些資料(透過作用域環境)與操控這些資料的函式相關聯。很明顯地,這與把一些資料(物件屬性)與一些方法的相關聯的物件導向程式設計(object-oriented programming)相似。</p>
<p>因此,在使用只含一個方法的物件之處,通常也可以使用閉包。</p>
<p>在 Web 中,試圖做這種事的情況還蠻普遍的。我們寫的大多數前端 JavaScript 程式碼屬於 event-based 的:我們定義了一些行為,接著把它與用戶觸發事件(例如點擊或按鍵)連結起來。程式碼通常會以 callback 的形式連結:也就是一個處理事件回應的函式。</p>
<p>例如,假設我們想在網頁上,加個能調整文字大小的按鈕。其中一個方法是用像素指定 <code>body</code> 元素的 font-size,接著透過相對的 em 單位,設置其他頁面的其他元素(如 headers)個大小:</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> 屬性(property)並藉由相對單位令頁面其他元素接受相應調整。</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> 現在變成能調整字體大小到 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/vnkuZ/7726/","","200")}}</p>
<h2 id="使用閉包模擬私有方法">使用閉包模擬私有方法</h2>
<p>諸如 Java 之類的程式語言,提供了私有方法宣告的能力,意味著它們只能被同一個 class 的其他方法呼叫。</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>該作用域環境由匿名函式的 body 建立,之後立刻執行。作用域環境還包括兩個私有項(private item):變數 <code>privateCounter</code> 與函式 <code>changeBy</code>。這些私有項,都不會在匿名函式外直接訪問。相反地,它們要透過由匿名包裝器(anonymous wrapper)回傳的公有函式訪問。</p>
<p>這三個公有函式,皆為共享同一個環境的閉包。由於 JavaScript 的語法作用域,它們都能訪問 <code>privateCounter</code> 變數與 <code>changeBy</code> 函式。</p>
<div class="note">
<p>你應該也發現到我們定義了建立 counter 的匿名函式、而我們接著呼叫它,並給<code>counter</code> 變數指派了回傳值。我們也能在分離的變數 <code>makeCounter</code> 儲存函式並用其建立數個 counter。</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();
alert(counter1.value()); /* Alerts 0 */
counter1.increment();
counter1.increment();
alert(counter1.value()); /* Alerts 2 */
counter1.decrement();
alert(counter1.value()); /* Alerts 1 */
alert(counter2.value()); /* Alerts 0 */
</pre>
<p>請注意 <code>counter1</code> 與 <code>counter2</code> 這兩個計數器是如何維護其獨立性的。每個閉包都以各自的閉包,在參照不同版本的 <code>privateCounter</code> 變數。每當呼叫其中一個計數器時,它會透過該變數的數值變更,改變語法作用域的環境。不過,在其中一個閉包的變數值改時,其他閉包的數值並不會受到影響。</p>
<div class="note">
<p>使用這種方法的閉包,提供了一些與物件導向程式設計的益處,尤其是資料隱藏與封裝。</p>
</div>
<h2 id="在迴圈建立閉包:一個常見錯誤">在迴圈建立閉包:一個常見錯誤</h2>
<p>在 ECMAScript 2015 導入 <a href="/zh-TW/docs/Web/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/v7gjv/", "", 200)}}</p>
<p><code>helpText</code> 陣列定義了三個有用的提示,每個提示都和文件內的輸入字段 ID 相關連。迴圈透過這三個定義,依序針對相對應的幫助方法(help method)添加了 <code>onfocus</code> 事件。</p>
<p>若試著運行這段程式碼,你會發現它不若預期般運行:無論聚焦哪一段,訊息都是在顯示你的年齡。</p>
<p>之所以如此,是因為指派到 <code>onfocus</code> 的函式為閉包,他們組成函式的定義、並從 <code>setupHelp</code> 的作用域捕抓函式的環境。三個閉包都被建立起來,但他們共享同一個能改變數值變數(<code>item.help</code>)的作用域環境。<code>item.help</code> 值早在執行 <code>onfocus</code> 回呼時,就已經被決定了。也由於 course 裡面的迴圈在那時已經執行了,給三個閉包共享的變數物件 <code>item</code> 早已離開 <code>helpText</code> 清單的 pointing to the last entry。</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/9573/", "", 200)}}</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);
}
})(); // Immediate event listener attachment with the current value of item (preserved until iteration).
}
}
setupHelp();</pre>
<p>如果你不想用更多閉包的話,你可以使用 ES2015 的 <code><a href="https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Statements/let">let</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)'}
];
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>,所以每個閉包都會與每個 block-scoped變數綁定,因而能在不用更多閉包的情況下完美運行。</p>
<h2 id="性能考量">性能考量</h2>
<p>如果指定的任務無須使用閉包的話,在其他函式內建立不必要的函式並不明智,因為從速度和記憶體角度而言,它都會影響腳本性能。</p>
<p>例如說,當我們建立了新的 object/class 時候,方法通常要和物件的原型(prototype)相關聯,而不是定義到物件的建構子(constructor)──這是因為每當建構子被呼叫的時候,方法都會重新分配(也就是說,它每次都在建立物件)。</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>以上的程式碼,可以寫得如同下例般簡潔:</p>
<pre class="brush: js">function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
(function() {
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}).call(MyObject.prototype);
</pre>
<p>在前例中,所有物件可共享繼承的原型,物件創立時也無須每次都定義方法。詳細資料請參見<a href="/zh-TW/docs/Web/JavaScript/Guide/Details_of_the_Object_Model">深入了解物件模型</a>。</p>
|