I never understood JavaScript closures (vi-version)
TIL
763
Male avatar

yiumih viết ngày 14/06/2018

Tôi chưa bao giờ hiểu JavaScript đóng cửa Cho đến khi ai đó giải thích cho tôi như thế này…

Dịch từ bài: I never understood JavaScript closures until someone explained it to me like this …

alt text

JavaScript closures luôn là một thứ gì đó rất huyền bí với tôi. Dù đã đọc nhiều bài viết khác nhau, đã sử dụng closures rất nhiều, thậm chí tôi đã sử dụng nó mà không hề biết là mình có sử dụng nó...

Những bài viết tác giả đề cập:

Gần đây gặp được một số người, nhờ cách giải thích của họ về closures nên cuối cùng tôi đã hiểu được nó. Hôm nay, trong bài viết này tôi sẽ dùng cách đó để giải thích lại cho các bạn về closures . Tôi xin cảm ơn đến những tác giả tại CodeSmith và series của họ là JavaScript The Hard Parts series.

Trước khi bắt đầu

Có một số khái niệm quan trọng chúng ta cần hiểu rõ trước khi tìm hiểu về closures. Một trong số đó chính là Execution Context

Tham khảo định nghĩa trong bài này Execution Context:

Xin code lại nguyên văn:

When code is run in JavaScript, the environment in which it is executed is very important, and is evaluated as 1 of the following:
Global code — The default environment where your code is executed for the first time.
Function code — Whenever the flow of execution enters a function body.
(…)
(…), let’s think of the term execution context as the environment / scope the current code is being evaluated in.

Đại ý là gì, chúng ta có thể hiểu khái niệm execution context là một môi trường, một phạm vi (environment/scope) của đoạn code đang được xét đến.

Xem hình để trực quan hơn:
alt text

Nói cách khác, khi bắt đầu một chương trình, chúng ta sẽ bắt đầu trong global execution context. Có một số biến lúc này được khai báo trong global execution context. Chúng ta thường gọi nó là biến toàn cục (global variables). Vậy khi chương trình gọi đến một function, đều gì sẽ xảy ra? Đó là:

  1. Đầu tiên, JavaScript tạo một cái execution context mới, gọi là local execution context.
  2. Local execution context này sẽ có một tập các biến riêng của nó, những biến này chỉ được sử dụng ở trong môi trường cục bộ đó mà thôi.
  3. Execution context mới này sẽ được đưa vào execution stack. Có thể hiểu execution stack là một cơ chế dùng để biết được nơi mà chương trình nó thực thi ở đâu.

Vậy khi nào thì một function kết thúc? Đó là lúc nó bắt gặp return hoặc là gặp closing bracket }. Khi một function đóng, nó sẽ xảy ra như sau:

  1. Các local execution contexts sẽ được xoá khỏi execution stack
  2. Những functions đã được 'triệu hồi' sẽ gửi giá trị trả về (returned value) cho context đã gọi nó (calling context). Context đó có thể là global execution context hoặc là một local execution context khác. Giá trị trả về sẽ được xử lý như thế nào thì tuỳ thuộc vào calling context tại thời điểm đó. Giá trị trả về có thể là một object, một array, một function, boolean hoặc một thứ gì đó bất kỳ. Nếu function không có return thì giá trị trả về sẽ là undefined
  3. Local execution context đó sẽ bị huỷ. Điều này rất quan trọng. Tất cả các biến trong đó đều được xoá khỏi bộ nhớ. Nó sẽ không còn tồn tại nữa. Đó cũng là lý do tại sao chúng ta gọi nó là biến cục bộ.

Một ví dụ đơn giản

Trước khi chúng ta bắt đầu về closures hãy khảo sát đoạn code sau đây. Nó rất rõ ràng và dễ hiểu với bất kỳ ai đang đọc bài này.

1: let a = 3
2: function addTwo(x) {
3:   let ret = x + 2
4:   return ret
5: }
6: let b = addTwo(a)
7: console.log(b)

Để hiểu được cách mà JavaScript engine nó work như thế nào, chúng ta hãy break down chi tiết nó ra:

  1. Dòng thứ nhất, chúng ta khai báo một biến mới trong global execution context và gắn giá trị cho nó là 3.
  2. Kế tiếp vấn đề bắt đầu xảy ra. Dòng 2 đến dòng 5 là định nghĩa của một function. Chúng ta khai báo một biến tên là addTwo trong global execution context. Và gán cho addTwo một cách thức hoạt động (assign to it a function defination). Những thứ nằm trong {} được gắn cho addTwo. Code bên trong chưa được đánh giá (evaluated), chưa được thực thic (executed) nó chỉ được lưu như là một biến để sử dụng trong tương lai.
  3. Bây giờ chúng ta bắt đầu đến dòng 6. Nhìn nó rất simple, nhưng có nhiều vấn đề cần nói đến ở đây. Đầu tiên chúng ta sẽ khai báo một biến mới trong global execution context và đặt tên nó là b. Ngay sau khi biến được khai báo nó được gán giá trị là undefined.
  4. Next, vẫn trên dòng 6, chúng ta thấy có một phép toán gán. Bây giờ chúng ta có thể gán giá trị mới cho biến b. Lúc này function addTwo sẽ được triệu hồi. Khi bạn thấy một biến mà theo sau đó là dấu (...) thì có nghĩa là một function đang được gọi (cái này tác giả nói kỹ quá, nhiều khi làm mình ráng hiểu sâu xa, đơn giản ở đây tác giả nói là dấu hiệu để biết một hàm đang được gọi, mà ai đọc bài này thì cái đó đương nhiên phải hiểu rồi :smile:). Sau đó thì như thế nào, mỗi function sẽ trả về một thứ gì đó (có thể là một giá trị (value), object hoặc undefined). Cái gì được trả về từ function addTwo sẽ được gán cho biến b.
  5. Àh, đầu tiên phải gọi function addTwo. JavaScript sẽ tìm trong global execution context một biến có tên là addTwo. Oh, có một cái, nó được tìm thấy trong bước 2 (từ dòng 2-5). Nhìn xem, biến addTwo chứa một định nghĩa về function. Lưu ý thêm rằng biến a lúc này là tham số được truyền vào function. JavaScript tìm một biếna trong global execution context, sau khi tìm ra được giá trị của nó là 3 và truyền 3 vào function. Lúc này function đã sẵn sàng để thực thi.
  6. Bây giờ execution context sẽ chuyển sang một local execution context mới đã được tạo ra và đặt tên là addTo execution context. Execution context này sẽ được đưa vào ngăn xếp để thực thi (call stack). Điều gì sẽ xảy ra đầu tiên trong cái local execution context này?
  7. Bạn có thể bị mắc bẫy khi nói rằng: "một biến mới là ret sẽ được khai báo trong local execution context đầu tiên". Câu trả lời đúng là, chúng ta hãy xem tham số (parameters) của function trước tiên. Một biến có tên là x sẽ được khai báo trước tiên. Và vì giá trị được truyền vào function là 3 nên biến x sẽ được gán trị là 3.
  8. Bước tiếp theo là: biến ret được khai báo và gán giá trị là undefined (line 3).
  9. Vẫn dòng 3, cần thêm một điều nữa để thực thi. Đầu tiên là chúng ta cần giá trị của x. JavaScript sẽ tìm một biến tên là x. Nó sẽ tìm trong local execution context trước tiên. Và khi đã tìm thấy giá trị nó là 3. Toán tử thứ hai là 2. Kết quả của phép cộng là 5 sẽ được gán cho biến ret.
  10. Dòng 4. Giá trị của ret được trả ra cho function. Tìm kiếm trong local execution context ret có giá trị là 5. Function trả ra context bên ngoài một giá trị là 5. Function kết thúc.
  11. Dòng 4-5. Function kết thúc. Local execution context cũng bị huỷ. Biến xret bị xoá khỏi bộ nhớ. Nó không còn tồn tại nữa. Context này sẽ bị xoá khỏi ngăn xếp và giá trị trả về (5 - return value) sẽ được gửi đến context gọi nó (calling context). Trong trường hợp này, calling context chính là global execution context, bởi vì functionaddTwo đã được gọi từ global execution context.
  12. Bây giờ chúng ta tìm lại nơi mà chúng ta đã rời đi ở Bước 4. Giá trị trả về (5) được gán cho biến b. Chúng ta bắt vẫn đang ở dòng 6 của chương trình.
  13. Tôi sẽ không đi chi tiết ở dòng 7 này, giá trị của b sẽ được in ra và trong ví dụ này là số 5 sẽ được in ra.

Lời giải thích rất dài cho một chương trình rất đơn giản, và chúng ta vẫn còn chưa chạm đến closures. Chúng ta sẽ đề cập đến nó sau khi chúng ta đi một hoặc hai vòng nữa.

Lexical Scope

Chúng ta cần hiểu một số khía cạnh của Lexical scope. Hãy xem xét ví dụ sau:

1: let val1 = 2
2: function multiplyThis(n) {
3:   let ret = n * val1
4:   return ret
5: }
6: let multiplied = multiplyThis(6)
7: console.log('example of scope:', multiplied)

Câu chuyện ở đây là gì, chúng ta có những biến trong các local execution context và những biến trong global execution context. Một điều rắc rối của JavaScript là làm thế nào nó tìm được những biến mà nó cần. Nếu nó không tìm thấy biến trong local execution context của nó, thì nó sẽ tìm đến calling context. Và ở đó nếu cũng không tìm thấy thì nó sẽ tiếp tục tìm đến calling context của calling context đó. Cứ lặp đi lặp lại như thế cho đến khi nó gặp global execution context. Và nếu không gặp được biến cần tìm thì giá trị được trả về cho biến đó là undefined. Để hiểu rõ chúng ta hãy theo dõi ví dụ trên. Nếu bạn đã biết về scope thì có thể bỏ quan phần này.

  1. Khai báo một biến mới có tên là val1 trong global execution context và gán trị cho nó là 2.
  2. Dòng 2-5, khai báo một biến tên là multiplyThis và gán cho nó giá trị là định nghĩa của một function (function defination).
  3. Dòng 6, khai báo một biến mới multipled trong global execution context.
  4. Từ bộ nhớ global execution context, nhận lấy giá trị của multiplyThis và thực thi nó (vì nó là function). Truyền tham số là 6 cho function đó.
  5. Một function mới được gọi đồng nghĩa với một local execution context mới được tao ra.
  6. Trong local execution context đó, khai báo một biến n và gán giá trị là 6.
  7. Dòng 3. Trong local execution context, khai báo một biến tên là ret.
  8. Trên dòng 3 tiếp tục, thực hiện phép nhân với 2 toán tử là giá trị của 2 biến nval1. Tìm biến n trong local execution context. Nó đã được gắn ở bước 6 và giá trị của nó là 6. Tìm tiếp đến biến val1 trong local execution context. Trong local execution context hiện tại không có biến nào tên val1. Tìm đến calling context để hỏi thăm. calling context lúc này là global execution context. Tìm biến val1 trong đó. Ơn giời, cậu đây rồi. Nó đã được defiend trong bước 1. Giá trị của nó là 2.
  9. Lại tiếp tục trên dòng 3, thực hiện phép nhân và gán giá trị cho biến ret. 6 * 2 = 12. Bây giờ ret được gán trị là 12.
  10. Return biến ret. Local execution context bị huỷ, đồng thời biến retn cũng bị xoá khỏi bộ nhớ. Biến val1 không bị huỷ vì nó là biến nằm trong global execution context.
  11. Quay lại dòng 6. Trong calling context, biến multiplied được gán trị là 12.
  12. Cuối cùng trên dòng 7, chúng ta sẽ thấy giá trị của multiplied sẽ được console ra màn hình.

Trong ví dụ trên, chúng ta cần nhớ rằng một function ngoài việc nó có thể truy cập đến những biến của nó thì nó có thể truy cập đến những biến được defined trong calling context gọi đến function đó. Và người ta thường gọi nó với thuật ngữ là scope.

A function that returns a function

Trong ví dụ đầu tiên, function addTwo trả về một con số. Chúng ta nên nhớ rằng một function có thể trả về bất cứ thứ gì. Hãy xem qua một ví dụ mà một function có giá trị trả về là một function, đây là việc cần thiết để hiểu về closures. Sau đây là ví dụ mà chúng ta sẽ phân tích.

 1: let val = 7
 2: function createAdder() {
 3:   function addNumbers(a, b) {
 4:     let ret = a + b
 5:     return ret
 6:   }
 7:   return addNumbers
 8: }
 9: let adder = createAdder()
10: let sum = adder(val, 8)
11: console.log('example of function returning a function: ', sum)

Let’s go back to the step-by-step breakdown.
Nào, chúng ta hãy cùng phân tích từng bước một.

  1. Dòng 1, chúng ta khai báo một biến tên là val trong global execution context và gán trị là 7
  2. Dòng 2-8, chúng ta khai báo một biến tên là createdAdder trong global execution context và gán giá trị cho nó là định nghĩa của một function (a funciton definition). Từ dòng 3-7 mô tả cách mà function sẽ làm gì. Cũng như trước đó, chúng ta sẽ nhảy qua đoạn này. Định nghĩa của function này chỉ đơn giản được lưu vào biến createdAdder.
  3. Dòng 9. Chúng ta khai báo một biến mới là adder, trong global execution context có giá trị là undefined.
  4. Vẫn ở dòng 9. Chúng ta thấy dấu ngoặc tròn (); có nghĩa là chúng ta sẽ thực thi một function. Tìm trong bộ nhớ global execution context chúng ta thấy một biến tên là createAdder. Nó đã được tạo ở bước 2. Ok, bắt đầu thực thi nó.
  5. Thực thi một function. Bây giờ chúng ta đang ở dòng 2. Một local execution context được tạo ra. Chúng ta có thể tạo những biến cục bộ trong execution context mới. JavaScript thêm execution context mới này vào call stack. Do funciton này không có tham số truyền vào nên chúng ta sẽ nhảy đến phần thân của function.
  6. Vẫn trên dòng 3-6, chúng ta có một function mới được khai báo. Chúng tạo một biến tên là addNumbers trong local execution context. Điều này rất quan trọng. addNumbers chỉ tồn tại ở trong local execution context. Định nghĩa của function này sẽ được lưu vào biến addNumbers.
  7. Bây giờ chúng ta đang ở dòng 7. Giá trị trả về là biến addNumbers. JavaScript tìm biến có tên là addNumbers. Nó là một function. Fine, một function có thể trả ra bất cứ thứ gì kể cả một function. Do đó giá trị trả về chính là định nghĩa của addNumbers. Và sau đó, bất cứ thứ gì nằm trong dòng 4-5 sẽ bị xoá bỏ khỏi call stack.
  8. Phía trên return, local execution context đã bị huỷ. Biến addNumbers cũng không còn nữa. Tuy nhiên definition của function đó vẫn có tồn tại, nó được trả về sau khi gọi thực thi function createdAdder và đã được gán cho biến adder. Đó là biến đã được tạo tại bước 3.
  9. Bắt đầu đến dòng 10. Chúng ta định nghĩa một biến mới tên là sum trong global execution context với giá trị ban đầu là undefined.
  10. Kế tiếp chúng ta cần thực thi function. Function nào? Đó là function đã được defined tên là adder. Tìm nó trong global execution context, chắc chắn chúng ta sẽ tìm thấy nó. Nó là function có 2 tham số.
  11. Nhận 2 tham số truyền vào và gọi function để thực thi. Đầu tiên là biến val, cái này đã được defined trong bước 1, nó đại diện cho số 7 và tham số truyền vào thứ hai là số 8.
  12. Bây giờ chúng ta thực thi function. Function này được defined từ dòng 3-5. Một cái local execution context được tạo ra. Bên trong local context này có 2 biến mới được tạo là ab. Chúng được gán tương ứng giá trị là 78, khi những tham số được truyền vào function ở bước trên.
  13. Dòng 4, một biến mới được khai báo và đặt tên là ret. Đó được khai báo báo trong local execution context.
  14. Dòng 4, thực hiện phép tính cộng, với giá trị của ab. Kết quả của phép toán là 15 được gán cho biến ret
  15. Biến ret được trả về từ function. Local execution context bị huỷ, nó bị xoá khỏi call stack, các biến a,bret cũng không còn nữa.
  16. Giá trị trả về được gán cho biến sum chúng ta đã khai báo ở bước 9.
  17. Chúng ta in giá trị của sum ra console.

Như mong muốn, console sẽ in ra là 15. Chúng ta đã đi vòng vòng qua các ví dụ. Bây giờ tôi sẽ nhấn mạnh lại một số điểm quan trọng. Đầu tiên, định nghĩa của một function có thể lưu vào một biến, function đó có thể ko xuất hiện trong chương trình cho đến khi nó được gọi đến. Thứ hai, mỗi lần function được gọi, một local execution context (tạm thời) sẽ được tạo. Execution context đó sẽ 'tan biến' khi function kết thúc. Một function kết thúc khi nó gặp return hoặc closing bracket }.

Cuối cùng là closure

Nhìn đoạn code kế tiếp và thử tìm hiểu xem điều gì sẽ xảy ra.

 1: function createCounter() {
 2:   let counter = 0
 3:   const myFunction = function() {
 4:     counter = counter + 1
 5:     return counter
 6:   }
 7:   return myFunction
 8: }
 9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)

Đến bây giờ chúng ta đã quen với nó từ 2 ví dụ trước, hãy thực thi nhanh đoạn code trên.

  1. Dòng 1-8. chúng ta tạo một biến mới là createdCounter trong global execution context và gán cho nó là định nghĩa của một function.
  2. Dòng 9. chúng ta khai báo một biến tên là increment trong global execution context.
  3. Dòng 9 tiếp tục. Chúng ta gọi đến function createCounter và gán giá trị được trả về từ function cho biến increment.
  4. Dòng 1-8. Gọi đến function này. Tạo ra một local execution context mới.
  5. Dòng 2. Trong local execution context khai báo một biến mới tên là counter và gán giá trị là 0.
  6. Dòng 3–6. Khai báo một biến mới tên là myFunction. Biến này được khai báo trong local execution context. Giá trị của biến là một definition function khác. Đã defined từ dòng 4-5.
  7. Dòng 7. Trả về giá trị của biến myFunction. Local execution context đã bị xoá. myFunctioncounter không còn tồn tại. Chương trình sẽ trả giá trị về cho calling context.
  8. Dòng 9. Từ calling context cũng chính là global execution context, giá trị trả về từ function createCounter đã được gán vào biến increment. Biến increment bây giờ là một function definition. Function definition này đã được trả về từ function createCounter. Không còn function nào có tên myFunction nữa, nhưng definition giống như thế vẫn còn. Bên trong global context, nó được gọi làincrement.
  9. Dòng 10. khai báo một biến mới tên là c1.
  10. Dòng 10 (continued). Tìm kiếm biến có tên increment, nó là một function, gọi nó. Nó là một function đã được trả về trước đó và function đó được định nghĩa từ dòng 4–5.
  11. Tạo một execution context mới. Không có tham số. Thực thi function.
  12. Dòng 4. counter = counter + 1. Tìm kiếm biến có tên là counter trong local execution context. Chúng ta vừa tạo trong context đó và chưa hề khai báo biến cục bộ. Không có biến cục bộ nào có tên là counter. Nhìn lại trong global execution context cũng không có biến nào tên là counter. JavaScript sẽ xem phép toán như sau counter = undefined + 1, khai báo một biến mới tên là counter và gán trị là 1, vì undefined xem như là 0.
  13. Dòng 5. Chúng ta trả về giá trị counter, hoặc là giá trị là 1. Chúng ta sẽ huỷ local execution context, bao gồm cả biến counter.
  14. Về lại dòng 10. Trả giá trị 1 về và gán cho biến c1.
  15. Dòng 11. Chúng ta sẽ lặp lại tương tự bước 10-14, và biến c2 cũng sẽ được gán trị là 1.
  16. Dòng 12. Chúng ta sẽ lặp lại tương tự bước 10-14, và biến c3 cũng sẽ được gán trị là 1.
  17. Dòng 13. Chúng ta sẽ log ra console các biến c1, c2c3.

Hãy tự mình thử xem và điều gì sẽ xảy ra. Bạn sẽ không nhận được log là 1, 11 như bạn mong muốn từ sự giải thích trên của tôi. Thay vào đó bạn sẽ thấy log là 1, 23. Tại sao lại như vậy?
Bằng cách nào đó, function increment nhớ được giá trị của biến counter. Làm thế nào để nó làm được điều đó?

Biến counter có phải là một phần của global execution context. Thử console.log(counter) xem sao và bạn sẽ nhận được là undefined. Vậy thì không phải rồi.

Có lẽ, khi gọi function increment, bằng cách nào đó nó quay lại function đã tạo trước đó createCounter?
Làm thế nào để thực hiện được điều đó? Giá trị biến increment là một function definition, không thể đến từ đó. Vậy thì điều này cũng không đúng

Vậy thì phải có một cơ chế khác. Đó là Closure. Cuối cùng chúng ta đã hiểu, thiếu điều này.

Đây là cách nó hoạt động của nó. Khi bạn khai báo một function mới và gán nó cho một biến, bạn lưu nó một function definition, cái này gọi là closure. Closure chứa tất cả các biến mà trong phạm vi (scope) tại thời điểm tạo function đó. Nó giống như một cái ba-lô. Một function definition xem như là một ba-lô nhỏ. Và trong đó chứa tất cả các biến mà trong scope tại thời điểm mà function definition được tạo ra.

Vì vậy cách giải thích ở trên sai hoàn toàn, hãy thử lại, nhưng lần này thì đúng.

 1: function createCounter() {
 2:   let counter = 0
 3:   const myFunction = function() {
 4:     counter = counter + 1
 5:     return counter
 6:   }
 7:   return myFunction
 8: }
 9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
  1. Dòng 1-8. chúng ta tạo một biến mới là createdCounter trong global execution context và gán cho nó là định nghĩa của một function. Như trên.
  2. Dòng 9. chúng ta khai báo một biến tên là increment trong global execution context. Như trên.
  3. Dòng 9 tiếp tục. Chúng ta gọi đến function createCounter và gán giá trị được trả về từ function cho biến increment. Như trên.
  4. Dòng 1-8. Gọi đến function này. Tạo ra một local execution context mới. Như trên.
  5. Dòng 2. Trong local execution context khai báo một biến mới tên là counter và gán giá trị là 0. Như trên.
  6. Dòng 3–6. Khai báo một biến mới tên là myFunction. Biến này được khai báo trong local execution context. Giá trị của biến là một definition function khác. Đã defined từ dòng 4-5. Bây giờ chúng ta cũng tạo một closure và include nó như là một phần của function definition. Closure chứ tất cả các biến trong scope, trong case này là biến counter có giá trị là 0.
  7. Dòng 7. Trả về giá trị của biến myFunction. Local execution context đã bị xoá. myFunctioncounter không còn tồn tại. Chương trình sẽ trả giá trị về cho calling context. Vậy chúng đang trả về function definitionclosure của nó, cái ba-lô mà nó chứa các biến nằm trong scope lúc được tạo ra.
  8. Dòng 9. Từ calling context chính là global execution context, giá trị trả về bởi createCounter được gán cho increment. Biến increment bây giờ chứa một function definition (và closure). Cái function definition này đã được tạo ra bơi createCounter. Không còn tồn tại function nào tên myFunction nữa, nhưng definition thì giống nhau. Trong global context, nó được gọi là increment.
  9. Dòng 10. Khai báo một biến mới c1.
  10. Dòng 10 (continued). Tìm kiếm biến có tên increment, nó là một function, gọi nó. Nó là một function đã được trả về trước đó và function đó được định nghĩa từ dòng 4–5. (và nó cũng có một ba-lô với các biến bên trong).
  11. Tạo một execution context mới. Không có tham số. Thực thi function.
  12. Dòng 4. counter = counter + 1. Chúng ta cần tìm biến counter. Trước khi chúng ta tìm trong local hoặc global execution context, hãy nhìn trong backpack của chúng ta trước. Hay nói cách khác là check trong closure. Nhìn xem, chúng ta closure có chứa biến tên là counter, nó có giá trị là 0. Sau đó thực hiện phép tính và kết quả là 1. Và nó sẽ được lưu lại trong closure. Closure bây giờ chứa biến counter có giá trị mới là 1.
  13. Dòng 5. Chúng ta trả về giá trị của counter với giá trị là 1. Sau đó huỷ local execution context.
  14. Quay lại dòng 10. Giá trị trả về là 1 được gán cho biến c1.
  15. Dòng 11. Chúng ta lặp lại 10-14. Lúc này khi tìm trong closure, chúng ta thấy rằng biến counter có giá trị là 1. Nó đã được set trong Bước 12 hay dòng 4 của chương trình. Giá trị của nó đã được tăng lên là 2 và lưu vào closure của function increment. Và biến c2 có giá trị là 2.
  16. Dòng 12. Lặp lại từ bước 10-14, c3 nhận được giá trị là 3.
  17. Dòng 13. Chúng log ra console giá trị của c1, c2c3.

Bây giờ chúng ta đã hiểu nó hoạt động như thế nào. Điều quan trọng chúng ta cần nhớ là khi một function được khai báo, nó sẽ bao gồm cả function definitionclosure của nó. Closure là một tập tất cả các biến trong scope tại thời điểm tạo ra function

Bạn có thể hỏi, có phải bất kỳ function nào cũng có closure, kể cả những function trong global scope? câu trả lời có Yes. Các functions được tạo trong global scope cũng tạo ra một closure. Nhưng lúc những functions này được tạo trong global scope, thì nó có thể access đến tất cả các biến trong global scope. Và khái niệm closure thật sự không còn liên quan nữa.

Khi giá trị trả về của một function là một function, thì lúc đó chúng ta sẽ nhìn thấy khái niệm closures rõ nhất. Function mà nó trả về có thể access đến những biến mà không có trong global scope, nhưng lại có ở trong closure của function đó.

Not so trivial closures

Đôi khi closures xuất hiện nhưng bạn không hề nhận ra nó. Bạn có thể đã thấy một ví dụ về những gì chúng ta gọi là một phần ứng dụng. Giống như trong đoạn mã sau.
Sometimes closures show up when you don’t even notice it. You may have seen an example of what we call partial application. Like in the following code.

let c = 4
const addX = x => n => n + x
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)

Trong trường hợp trên dùng arrow function có thể làm bạn khó hiểu, đây là đoạn code tương ứng.

let c = 4
function addX(x) {
  return function(n) {
     return n + x
  }
}
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)

Chúng ta khai báo một function tính tổng tổng quát tên là addX có một tham số là x và trả về là một funciton khác.

Function trả về cũng có một tham số và tham số đó được cộng với biến x.
Biến x là một phần của closure. Khi biến addThree đã được khai báo trong local context, nó được gán một function definition và một closure. Closures có chứa biến x.

Vậy bây giờ khi addThree được gọi và thực thi, nó có thể access đến biến x từ chính closure của nó và biến n được truyền vào function và tổng của nó sẽ được trả về.

Trong ví dụ này console sẽ in ra số 7.

Kết luận

Cách mà tôi sẽ luôn luôn nhớ về closures là nghĩ nó tương tự như cái ba-lô. Khi một function được tạo và truyền qua truyền lại hoặc trả về cho function khác, nó sẽ mang theo một ba-lô. Và trong ba-lô đó là tất cả các biến trong scope từ lúc function được khai báo.

Dịch sao cho chuẩn

This article has a very good primer on Execution Context. To quote the article:

Vocabularies

In order to understand
Flash forward
zip through
hang of it

Learn English by translating
yiumih 11-06-2018

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

Male avatar

yiumih

2 bài viết.
0 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
Male avatar
1 0
JavaScript Closures notes Reference https://medium.com/dailyjs/ineverunderstoodjavascriptclosures9663703368e8 https://medium.freecodecamp.org...
yiumih viết hơn 2 năm trước
1 0
Bài viết liên quan
White
0 4
fCC: Technical Documentation Page note So I have finished the HTML part of this exercise and I want to come here to lament about the lengthy HTML ...
HungHayHo viết hơn 2 năm trước
0 4
White
4 0
I used Spring boot, Hibernate few times back then at University, I'v started using it again recently. In this (Link), I want to check how Spring J...
Rey viết hơn 1 năm trước
4 0
White
23 1
Toán tử XOR có tính chất: + A XOR A = 0 + 0 XOR A = A Với tính chất này, có thể cài đặt bài toán sau với độ phức tạp O(N) về runtime, và với O(1)...
kiennt viết gần 4 năm trước
23 1
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


Male avatar
{{userFollowed ? 'Following' : 'Follow'}}
2 bài viết.
0 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!