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
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
|
---
title: Closures
slug: Web/JavaScript/Closures
translation_of: Web/JavaScript/Closures
---
<div>{{jsSidebar("Intermediate")}}</div>
<p class="summary"><em>Closure</em> là một hàm được viết lồng vào bên trong một hàm khác (hàm cha) nó có thể sử dụng biến toàn cục, biến cục bộ của hàm cha và biến cục bộ của chính nó (lexical scoping)</p>
<h2 id="Lexical_scoping">Lexical scoping</h2>
<p>Xem xét ví dụ sau</p>
<div>
<pre class="brush: js">function init() {
var name = 'Mozilla'; // name là biến cục bộ của hàm init
function displayName() { // displayName() là hàm closure
alert(name); // sử dụng biến của hàm cha
}
displayName();
}
init();</pre>
</div>
<p><code>init()</code> tạo một biến cục bộ <code>name</code> và một hàm <code>displayName()</code>. Hàm <code>displayName()</code> được khai báo bên trong hàm <code>init()</code> và chỉ tồn tại bên trong hàm <code>init()</code> . Hàm <code>displayName()</code> không có biến cục bộ nào của chính nó. Tuy nhiên, hàm <code>displayName()</code> truy cập đến biến <code>name</code> vốn được định nghĩa ở hàm cha, <code>init()</code>. Nếu bên trong hàm <code>displayName()</code> có khai báo biến cục bộ của chính nó, biến đó sẽ được sử dụng.</p>
<p>{{JSFiddleEmbed("https://jsfiddle.net/78dg25ax/", "js,result", 250)}}</p>
<p><a href="http://jsfiddle.net/xAFs9/3/" title="http://jsfiddle.net/xAFs9/">Thực thi</a> đoạn code trên sẽ nhận được kết quả từ <code>alert()</code> bên trong hàm <code>displayName()</code> , giá trị biến <code>name</code> . Đây là một ví dụ của <strong><em>lexical</em> <em>scoping</em></strong>, cách các biến được truy cập như thế nào khi hàm được lồng nhau. Hàm lồng bên trong có thể truy suất đến biến được khai bào từ hàm bên ngoài.</p>
<h2 id="Closure">Closure</h2>
<p>Giờ xem xét đến ví dụ sau:</p>
<pre class="brush: js">function makeFunc() {
var name = 'Mozilla';
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
</pre>
<p>Chạy đoạn code trên sẽ nhận kết quả tương tự như ví dụ hàm <code>init()</code> ở trên; sự khác nhau ở đây là gì? khi gọi hàm makeFunc<code>()</code> sẽ return về hàm <code>displayName()</code> , và chưa hề chạy qua đoạn code trong hàm <code>displayName()</code>.</p>
<p>Thoạt nhìn, đoạn code này sẽ không dễ nhận ra đoạn code này vẫn chạy bình thường. Trong một số ngôn ngữ lập trình khác, biến cục bộ bên trong một hàm chỉ tồn tại trong quá trình hàm thực thi. Một khi <code>makeFunc()</code> chạy xong, chúng ta sẽ nghĩ rằng biến <code>name</code> sẽ không còn thể truy cập được. Tuy nhiên, đoạn code trên sẽ vẫn cho ra kết quả không khác gì ví dụ ở trên cùng, rõ ràng đây là một tính chất đặc biệt của Javascript.</p>
<p>Trong trường hợp này, <code>myFunc</code> đang tham chiếu đến một instance <code>displayName</code> được tạo ra khi chạy <code>makeFunc</code>. Instance của <code>displayName</code> sẽ duy trì lexical environment, biến <code>name</code> sẽ vẫn tồn tại. Với lý do này, khi gọi hàm <code>myFunc</code> , giá trị biến <code>name</code> vẫn có và chuỗi "Mozilla" sẽ được đưa vào hàm <code>alert</code>.</p>
<p>Một ví dụ thú vị khác — hàm <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>Trong ví dụ này, chúng ta định nghĩa hàm <code>makeAdder(x)</code>, nhận vào 1 argument, <code>x</code>, và trả về một hàm khác. Hàm trả về nhận vào 1 argument, <code>y</code>, và trả về kết của của <code>x</code> + <code>y</code>.</p>
<p>Bản chất, <code>makeAdder</code> là một hàm factory — nó tạo ra một hàm khác nhận một argument. Ví dụ trên chúng ta sử dụng hàm factory để tạo ra 2 functions — cái thứ nhất thêm argument là 5, cái thứ 2 thêm 10.</p>
<p><code>add5</code> và <code>add10</code> đều là closures. Cùng một xử lý bên trong, nhưng được lưu ở lexical environments khác nhau. Trong lexical environment của <code>add5</code> , <code>x</code> = 5, trong khi lexical environment của <code>add10</code>, <code>x</code> = 10.</p>
<h2 id="Ứng_dụng_closures">Ứng dụng closures</h2>
<p>Closures hữu dụng vì nó cho phép chúng ta gắn một vài dữ liệu (bên trong lexical environment) với một function sẽ tương tác với dữ liệu. Tương tự như trong object-oriented programming, các object cho phép chúng ta gắn một vài dữ liệu với một hoặc nhiều phương thức bên trong</p>
<p>Trên nền web, hầu hết code được viết bằng JavaScript là event-based — chúng ta định nghĩa một xử lý, sau đó gắn nó vào event sẽ được gọi bởi user (ví dụ như click hay keypress). Đoạn code của chúng ta sẽ là callback: 1 function chạy khi có một sự kiện xảy ra.</p>
<p>Ví dụ, giả sử chúng ta muốn thêm một cái button để thay đổi kích thước chữ. Một trong những cách làm là set font-size cho thẻ <code>body</code> bằng giá trị pixels, sau đó set kích thước của những phần từ khác (như header) sử dụng đơn vị <code>em</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>Khi thay đổi <code>font-size</code> của thẻ <code>body</code> , kích thước font của h1, h2 sẽ tự động được điều chỉnh.</p>
<p>Trong 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>, và <code>size16</code> là những hàm sẽ thay đổi kích thước font chữ của body qua 12, 14, và 16 pixels. Gắn cho các button tương ứng:</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="Giả_lập_phương_thức_private_với_closures">Giả lập phương thức private với closures</h2>
<p>Những ngôn ngữ như Java chúng ta có cách để khai báo các phương thức private, nghĩa là phương thức chỉ được gọi bởi các phương thức khác nằm cùng class.</p>
<p>JavaScript không hỗ trợ cách làm chính quy cho việc này, tuy nhiên có thể giả lập việc này bằng closures. Phương thức Private không chỉ hữu dụng trong việc giới hạn việc truy cập: nó còn là một cách rất tốt để quản lý global namespace, giữ các phương thức không cần thiết có thể làm xáo trộn những phương thức public.</p>
<p>Đoạn code bên dưới diễn giải cách sử dụng closures để khai báo một phương thức public có thể truy cập phương thức private và biến. Sử dụng closures như thế này gọi là <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>Mỗi closure có một lexical environment. Ở đây, chúng ta tạo 1 lexical environment cho cả 3 function: <code>counter.increment</code>, <code>counter.decrement</code>, and <code>counter.value</code>.</p>
<p>Lexical environment được tạo bên trong một hàm không tên function, sẽ được tạo ra ngay khi được gán cho một khai báo. Lexical environment chứa 2 private: biến <code>privateCounter</code> và hàm <code>changeBy</code>. Cả 2 đối tượng private đều không thể được truy cập trực tiếp từ bên ngoài. Thay vào đó, nó chỉ có thể tương tác thông qua 3 phương thức public.</p>
<p>Cả 3 phương thức public đều là closures chia sẽ cùng 1 Lexical environment. Cả 3 đều có thể truy cập đến <code>privateCounter</code> và <code>changeBy</code></p>
<div class="note">
<p>Chúng ta khai báo một hàm không tên tạo counter, và gọi nó ngay lập tức rồi gắn vào biến <code>counter</code> . Chúng ta lưu hàm này vào một biến khác <code>makeCounter</code> và sử dụng nó để tạo ra nhiều counter khác</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>Để ý cách 2 counters, <code>counter1</code> và <code>counter2</code>, hoàn toàn độc lập với nhau. Mỗi closure tham chiếu đến các instance khác nhau của <code>privateCounter</code> .</p>
<div class="note">
<p>Sử dụng closures bằng cách này cho ta rất nhiều ưu điểm như trong object-oriented programming -- cụ thể, dữ liệu được ẩn đi và đóng gói.</p>
</div>
<h2 id="Closure_Scope_Chain">Closure Scope Chain</h2>
<p>Mỗi closure chúng ta có 3 scopes:-</p>
<ul>
<li>Scope cục bộ</li>
<li>Scope của function chứa closure</li>
<li>Scope global</li>
</ul>
<p>Chúng ta có thể truy cập đến cả 3 scope này trong closure tuy nhiên sẽ ra sau nếu chúng lồng nhiều closure với nhau. Như ví dụ sau:</p>
<pre class="brush: js">// global scope
var e = 10;
function sum(a){
return function(b){
return function(c){
// outer functions scope
return function(d){
// local scope
return a + b + c + d + e;
}
}
}
}
console.log(sum(1)(2)(3)(4)); // log 20
// chúng ta có thể không dùng hàm không tên:
// global scope
var e = 10;
function sum(a){
return function sum2(b){
return function sum3(c){
// outer functions scope
return function sum4(d){
// local scope
return a + b + c + d + e;
}
}
}
}
var s = sum(1);
var s1 = s(2);
var s2 = s1(3);
var s3 = s2(4);
console.log(s3) //log 20
</pre>
<p>Với ví dụ trên, chúng ta có thể nói toàn bộ closure sẽ có cùng scope với function cha.</p>
<h2 id="Tạo_closures_trong_vòng_lặp_lỗi_thường_thấy">Tạo closures trong vòng lặp: lỗi thường thấy</h2>
<p>Trước khi có từ khóa <a href="/en-US/docs/Web/JavaScript/Reference/Statements/let" title="let"><code>let</code> keyword</a> được giới thiệu trong ECMAScript 2015, một lỗi thường gặp trong closure khi nó được tạo bên trong vòng lặp. Xem ví dụ sau:</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/8164/", "", 200)}}</p>
<p>Mảng <code>helpText</code> khai báo 3 string help, tương ứng cho mỗi ID của input. Vòng lặp chạy qua cả 3 khai báo này, chèn vào sự kiện <code>onfocus</code> để hiển thị đoạn string phù hợp với từng input.</p>
<p>Nếu thử chạy đoạn code này, bạn sẽ thấy kết quả không giống như chúng ta nghĩ. Mặc cho chúng ta đang focus vào input nào, dòng message hiển thị sẽ luôn là "Your age (you must be over 16)".</p>
<p>Lý do là hàm gắn cho sự kiện <code>onfocus</code> là closures; nó sẽ thống nhất các khai báo trong và đưa vào chung scope của hàm <code>setupHelp</code>. Cả 3 closures được tạo trong vòng lặp, nhưng cùng chung lexical environment, tức là dùng chung biến <code>item.help</code>. Giá trị <code>item.help</code> được xác định khi <code>onfocus</code> được gọi. Vì ở đây vòng lặp đã chạy đến giá trị cuối cùng của mảng, biến <code>item</code> sẽ trỏ đến giá trị cuối cùng trong mảng.</p>
<p>Giải pháp trong tình huống này là dùng thêm một closures: như cách chúng ta viết function factory trước đó:</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/", "", 300)}}</p>
<p>Hàm <code>makeHelpCallback</code> đã tạo ra một<em> lexical environment</em> riêng cho mỗi callback.</p>
<p>Một cách khác là sử dụng closure không tên</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>Nếu không muốn sử dụng nhiều closure, có thể dùng từ khóa <code><a href="https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/let">let</a></code> được giới thiệu trong ES2015 :</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>Ví dụ này ta sử dụng <code>let</code> thay cho <code>var</code>, như thế mỗi closure được gán cho 1 biến block-scoped.</p>
<p>Một cách khác nữa là dùng <code>forEach()</code> để lặp qua mảng <code>helpText</code> và gắn hàm xử lý {{htmlelement("div")}}, như bên dưới:</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="Cân_nhắc_về_hiệu_năng">Cân nhắc về hiệu năng</h2>
<p>Dùng closure trong những trường hợp thực sự không cần thiết thì không khôn ngoan vì nó có thể ảnh hưởng hiệu năng lúc chạy.</p>
<p>Một ví dụ, khi tạo mới một object/class, phương thức thường nên gán vào object mà không nên khai báo bên trong hàm khởi tạo của object. Lý do là mỗi khi hàm constructor được gọi, phương thức sẽ được gán lại một lần nữa trên mỗi một object được tạo ra</p>
<p>Ví dụ cho trường hợp sau:</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>Bởi vì đoạn code trên không thực sự cần những lợi ích có được từ closure trên mỗi instance, chúng ta có thể viết lại mà không sử dụng closure:</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>Tuy nhiên, khai báo lại prototype không được khuyến khích. Chúng ta mở rộng prototype bằng cách sau:</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>Trong 2 ví dụ trên, tất cả object sẽ kế thừa cùng những prototype và khai báo phương thức trên mỗi object không bắt buộc. Xem <a href="/en-US/docs/Web/JavaScript/Guide/Details_of_the_Object_Model">Details of the Object Model</a> để tìm hiểu thêm</p>
|