Như ở trên thì Javascript Closures luôn có một chút gì đó bí ẩn với mình. Mình đã đọc rất nhiều bài viết từ tiếng ta đến tiếng tây ( hay do đọc chưa đủ nhiều nhỉ... ).
Mình cũng đã sử dụng Closures trong các dự án của mình và nhiều khi mình còn không nhận ra là mình đã sử dụng Closures.
Tuy nhiên, đòng đời xô đẩy đã đưa mình đến với
bài viết tiếng anh này, phương pháp giải thích trong bài viết đã thực sự có tác dụng với mình. Mình sẽ cố gắng sử dụng phương pháp này và những gì mình hiểu để giải thích về Closures.
Trước khi chúng ta bắt đầu
Có một vài khái niệm khá quan trọng chúng ta cần hiểu trước khi chúng ta tìm hiểu Closures. Đầu tiên là Execution Context ( Từ này được dich sang tiếng việt thì sẽ khá là ngang nên mình sẽ để nguyên ).
Bạn có thể đọc
bài viết này để thực sự hiểu về Execution Context.
Tóm tăt lại thì Execution Context là 1 vùng, 1 khu vực, 1 phạm vi , 1 vùng...vv.
Khi code được thực thi, vùng thực hiện hiện đoạn code đó vô cùng quan trọng, nó có thể nằm trong 2 vùng sau :
- Global Code - Vùng global, vùng có pham vi lớn nhất có ảnh hưởng tới toàn bộ các vùng khác nằm trong nó, bất kì file code nào cũng sẽ được thực thi trong vùng này đầu tiêu.
- Function Code - Vùng trong function, Khi code nằm trong một function nào đó được thực thi trong.
Vậy có thể hiểu Execution Context đó như 1 vùng/ 1 phạm vi hoạt động code Javascript của bạn.
Hay nói theo một các khác , khi chúng ta bắt đầu thực thi một đoạn code, thì đoạn code đó sẽ được bắt đầu trong vùng global . Một vài biến được khai báo trong vùng global thì chúng ta gọi đó là biến toàn cục.
Khi chương trình thực thi một function thì điều gì sẽ thực sự xảy ra:
1, Js sẽ tạo một vùng mới, riêng để chạy
2, Trong vùng đó sẽ được tạo một vài biến , những biến đó sẽ chỉ có thể hoạt động trong vùng vừa được tạo ra
3, Các vùng mới bao gồm code được viết giữa 2 dấu { } của 1 function , vừa được tạo đó sẽ được cho vào hàng đợi thực thi và đợi để được call.
Khi nào 1 function kết thúc đó là khi nó được kết thúc với return hoặc dấu ngoặc đóng }. Khi một function kết thúc thì:
1, Vùng thực thi function đó sẽ được xóa khỏi hàng đợi
2, Function gửi các giá trị trả về trong function về vùng gọi. Vùng gọi cũng chính là nơi gọi đến function này, nó có thể là vùng global, hoặc trong một vùng của 1 function nào đó.
Điều này tùy thuộc vào việc bạn gọi function đó ở đâu.
Giá trị trả về có thể là Object, Function, Boolean, ...
Nếu function không kết thúc với câu lệnh return thì undefined sẽ được trả về.
3, Vùng vừa được tạo sẽ bị xóa khỏi bộ nhớ. Điều này rất quan trọng, bạn hãy ghi nhớ nó :)
Tất cả các biến được khai báo trong vùng đó cũng sẽ bị xóa. Chúng không còn available nữa, điều này có thể giải thích tại sao chúng được gọi là biến cục bộ.
Một ví dụ đơn giản để minh họa cho sự phân biệt vùng miền 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)
OK, Chúng ta hãy phân tích từng dòng code theo các step sau:
1, Dòng 1 , chúng ta khai báo một biến mới a trong vùng global và gán cho nó giá trị là 3
2, Từ dòng 2 đến dòng 5, chúng ta khai báo 1 biến mới là addTwo trong vùng global. Chúng ta đã gán cho nó giá trị gì ? Đó là một định nghĩa function - một biến có kiểu giá trị là function.
Mọi thứ diễn ra giữa { } là để gán cho addTwo. Tuy nhiên code trong function đó chưa được thực thi, chỉ được lưu trữ trong addTwo để tương lai có thể sử dụng.
3. Giờ chúng ta đang ở dòng 6. Chúng ta khai báo b là một biến mới trong vùng global.
4, Vẫn trong dòng 6, chúng ta sẽ gán một giá trị mới cho b. Khi bạn thấy một biến và ngay sau đó là 2 dấu ( ), thì đó là dấu hiện một function được gọi để thực thi. Mọi function đều trả về một thứ gì đó (1 giá trị, 1 function cũng có thể là undefined). function addTwo được gọi, mọi thứ được trả về đều được gán cho b.
5, Khi gọi thực thi addTwo từ vùng global nên JS sẽ đi và tìm trong vùng global, 1 biến có tên là addTwo. Ồ, tất nhiên là nó sẽ tìm thấy vì nó đã được định nghĩa và ngồi đợi từ step 2 (dòng 2 -> dòng 5).
Biến addTwo đã được định nghĩa là một function. Chú ý răng khi gọi addTwo thì biến a đã được truyền vaò như 1 tham số cho function. JS 1 lần nữa đi tìm biến a trong vùng global, và nó đã tìm thấy biến a đang mang giá trị bằng 3. và truyền gía trị 3 vào làm tham sô của function. Tất cả mọi thứ đã sẵn sàng để được thực thi
6. Bây giờ vùng thực thi đã thay đổi. Một vùng mới được tạo ra. Chúng ta đặt tên cho nó là "vùng addTwo". vùng này được đẩy vào hàng đợi. Điều đầu tiên được làm trong vùng này là gì?
7, Đầu tiên, 1 biến ret được khai báo trong vùng addTwo?? Rất tiếc điều này đã sai. Đầu tiên Js sẽ tìm trong các tham số truyền vào trước. Một biến mới x được khai báo trong vùng này. Và giá trị 3 được truyền vào khi addTwo được gọi ,vậy x sẽ được gán cho giá trị bằng 3.
8, Bước tiếp theo mới là 1 biến ret đã được khai báo trong vùng addTwo, và giá trị của nó là ... undefined.
9. Cho đến hết dòng 3, một phép cộng sẽ được thực hiện. Đầu tiên chúng ta cần giá trị của x. JS tìm x, tìm thấy một và giá trị của nó bằng 3.
Hành động thứ 2 là + với số 2. Kết quả của 5, như vậy đến hết dòng 3 ret mới có giá trị của nó là 5. Function trả về 5. Và vùng addTwo bị xóa.
10. Dòng 4-5, vùng addTwo bị xóa, các biến x , ret bị xóa theo. addTwo được đưa xóa hàng đợi và được đặt vào trong vùng mới là vùng global.
Bởi vì function addTwo được gọi từ vùng global.
12, Một chuỗi các sự việc đã xảy ra nhưng chúng ta vẫn ở dòng 6 đó các bạn.
13, Trong dòng 7, nội dung của b được in ra console . Đó là 5
Giải thích mỏi cả tay cho 1 chương trình đơn giản, nhưng rất tiếc vẫn chưa có 1 tí nội dung nào liên quan đến Closures.
Vì vậy, tất nhiên chúng ta sẽ đến với ví dụ tiếp theo.
Lexical scope
(Google Translate mờ cả mắt đề có thể hiểu nó là phạm vi gì , nhưng có lẽ để nguyên tên cho e nó thì có lẽ dễ hiểu hơn :D )
Chúng ta cầ hiểu một vài khía cạnh của phạm vi Lexical.
Ví dụ 2 :
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)
Ý tưởng ở đây là chúng ta có những biến trong vùng local và các biến trong vùng global. Một sự phức tạp của Javascript đó là việc nó đi tìm kiếm các biến trong vùng. Nếu nó không thể tìm một biến trong vùng local thì nó sẽ tìm trong vùng gọi đến function đó.
Và nếu trong vùng để gọi mà cũng không thấy nó sẽ tìm trong vùng global. ( Và nếu nó không tìm thấy ở đây nữa, thì nó sẽ được gán giá trị là undefined).
Lấy ví dụ trên để giải thích luôn.
1, khai báo biến val1 trong vùng global và gán giá trị của nó cho 2.
2, Dòng 2 - 5. Khai báo 1 biến mới là multiplyThis và gán cho chúng một function.
3. Dòng 6, Khai báo một biến mới multiplied trong vùng global
4, Tìm biến multiplyThis từ vùng global, truyền số 6 là tham sô của nó và thưc thi nó
5. Một vùng local mới được tạo, tương tự như VD trên ta có vùng multiplyThis.
6, Trong vùng multiplyThis, khai báo một biến n và truyền cho nó giá trị 6.
7. Trong dòng 3. Trong vùng cục bộ khai báo 1 biến ret mới.
8. Tiếp tục trong dòng 3. Thực hiên phép nhân giữa n và val1. Tìm biến n trong vùng multiplyThis. Chúng ta đã khai báo nó ở bước 6.
Giá trị của nó là 6. Tìm biến val1 trong vùng multiplyThis. Vùng local không có biến val1. Check theo vùng gọi, ở đây vùng gọi đến function chính là vùng global. Vì vậy JS sẽ tìm biến val1 trong biến global. Tất nhiên nó sẽ tìm thấy, val1 được đc định nghiã ở step 1. Thực hiện phép nhân n và val1 được giá trị 12 và gán nó cho ret.
9, Vùng multiplyThis trả về biến ret, nó sẽ bị xóa cùng tất cả các biến ret, n... ngoại trừ val1 bởi vì nó đến từ vùng global.
10, Trở về dòng 6, sau khi được goi, giá trị 12 được gán cho biến multiplied.
11, Cuối cùng trong dòng 7, multiplied được in ra console.
Vậy trong ví dụ này chúng ta cần nhớ 1 function đã truy cập vào 1 biến được khai báo ngoài vùng local của nó (biến val1). Để tưởng nhớ đến hiện tượng này , người ta đã đặt tên cho nó là Lexical scope
Một function trả về một function.
Trong ví dụ đầu tiên về function addTwo trả về một số. Còn trong ví dụ này function sẽ trả về 1 function, ví dụ này sẽ giúp chúng ta đến gần hơn với Closures. Đây là ví dụ 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)
1, Dòng 1, khai báo biến val và gán giá trị cho nó bằng 7.
2, Dòng 2 - 8, chúng ta khai báo một biến có tên là createAdder trong vùng global và chúng ta gán giá trị cho nó là 1 function.
Từ dòng 3 - 7 cho thấy rằng một function được định nghĩa. Ở đây, tất cả đều lưu trữ trong biến createAdder.
3, Dòng 9, chúng ta thấy (); có nghĩa là 1 biến function được gọi để thực thi.
JS tìm trong vùng global, tìm thấy biến createAdder. OK, gọi và thực thi nó.
4. Function createAdder được gọi, vùng createAdder được tạo. JS add vùng này vào hàng đợi. Function k có tham số nên bược đầu tiên sẽ là nhảy thẳng vào body của nó.
5, Gọi một function. Giờ chúng ta đang ở dòng 2 Chúng ta tiếp tục các bước tạo vào gán giá trị function cho biến addNumbers
6. Ở dòng 7, Chúng ta trả về biến addNumbers. JS sẽ tìm biến addNumbers. Đó là một định nghĩa function. Tốt, một function có thể return mọi thứ, kể cả một function. Vì chúng ta trả về function addNumbers. Sau khi return, chúng ta cũng remove vùng createAdder khỏi hàng đợi.
7. Sau khi remove vùng createAdder, biến addNumbers không còn tồn tại nữa.
Định nghiã hàm vẫn còn tồn tại, nó trả về từ function và nó truyền cho biến adder.
8. Bây giờ chúng ta đang ở dòng 10. Chúng ta khai báo một biến mới là sum trong vùng global. Và tạm thời cho nó giá trị là undefined.
9. Chúng ta cần thức hiện function. Function nào? functiong được defined trong biến adder. Chúng ta tìm trong vùng global và chắc chắn chúng ta sẽ tìm thấy nó. Nó là một function có 2 tham số.
10. Truyền vào function 2 tham số đó, xong chúng ta có thể gọi function và các tham số đúng. Đầu tiên là biến val, các chúng ta đã defined ở bước 1, nó mang giá 7 và cái thứ 2 là số 8.
11. Bây giờ chúng ta sẽ thực hiện function. Định nghĩa function là từ dòng 3 đến 5. Một vùng local mới được tạo. Trong vùng đó, 2 biến mới được tạo là a và b.
Chúng được lần lượt truyền 2 tham số 7 và 8. đó là những tham số chúng ta truyền vào ở những bước trước
12. Dòng 4. Một biến mới được khai bao , đặt tên là ret. Nó được khai báo trong một vùng createAdder
13. Chạy hết dòng 4, Phép cộng được thực hiên , nơi chúng ta có thể cộng được giá trị của a và b.
Kết quả của phép cộng (15) được gán cho biến ret.
14, Biến ret được trả về từ function đó. vùng createAdder bị xóa , chúng bị remove khỏi hàng đợi, biến a và b, ret không còn tổn tại.
15. Giá trị trả về được truyền cho biến sum , chúng được defined ở bước 9
17. Chúng ta in ra giá trị của sum.
Như mong muốn, console đã in ra 15. Chúng ta thực sự đã trải qua 1 loạt kiến thức ở đây, có một số điểm cần lưu ý
- Function được khai bảo có thể đc lưu trữ trong 1 biến, chỉ lưu trữ k thực thi điều gì cho đến khi được gọi
- Mỗi khi 1 function được gọi, một vùng của function đó được tạo ra trong bộ nhớ.
- Vùng của function đó sẽ bị xóa khi nó được kết thúc bởi return và dấu }
Cuối cùng, Closures (cuối cùng thì cũng đến ...)
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)
Chúng ta đã trải qua 2 ví dụ trước.. vì vậy hãy giải thích ngắn gọn cách chạy đoạn code này.
1. Từ dòng 1 đến dòng 8, Chúng ta tạo một biến mới là createCounter trong vùng toàn cục và gán giá trị cho nó là 1 function.
2. Dòng 9. Chúng ta khai báo một biến mới là increment trong vùng global.
3. Dòng 9 tiếp tục. Chúng ta gọi function createCounter và truyền giá trị trả về vào biến increment.
4. Dòng 1 đến dòng 8. Tạo vùng createCounter.
5. Dòng 2, bên trong vùng local createCounter, khai báo 1 biến mới có tên là counter và gán giá trị là 0 cho counter.
6. Dòng 3 đến 6, khai báo biến mới có tên là myFunction. Biến này được khai bao trong vùng myFunction.
7. Dòng 7, vùng createCounter trả về biến myFunction, các biến vừa được tạo mới trong trong vùng myFunction sẽ bị xóa.
8. Dòng 9. Trong hàng vùng goi function createCounter chính là vùng global, giá trị được trả về bởi createCounter được trả về cho increment. Biến increment hiện đang chứa một function , nó không còn có tên là myFunction nhưng cùng 1 định nghĩa.
9. Dòng 10, khai báo biến mới c1.
10. Dòng 10 tiếp tục. Tìm biến increment, thực hiện nó.
11, Tạo 1 vùng mới ta gọi nó là vùng increment. Không có tham số truyền vào nên bắt đầu thực thi function.
12. Dòng 4, counter = counter + 1. JS tìm biến counter trong vùng cục bố là vùng increment trước. Không biến counter nào được tìm thấy.
Js tiếp tục tìm trong vùng gọi đến function increment đó là vùng global , cũng không biến counter nào được khai báo.
Vì vậy dòng code counter = counter + 1, tương đương với counter = undefined + 1. Vì undefined trong JS tương ứng với 0 nên kết quả của phép tính là 1.
13. Dòng 5. Chúng ta trả về giá trị của counter hay là số 1. Chúng ta xóa vùng increment và biến counter.
14. Trở về dòng 10. Giá trị trả về là 1 và assigned cho c1.
15. Dòng 11 . Lặp lại 10 - 14. C2 đc gán giá trị là 1.
16. Dòng 12. Lặp lại 10 - 14. c3 được gán giá trị 1.
17. Dòng 13. 3 số 1 được log trên console.
Nào hãy mở console và chạy thử đoạn code này nào, ... Ta da và kết qả trả về là : ...
'example increment'
1
2
3
What the f*ck happen??
Bằng cách nào mà giá trị biến counter được lưu trữ lại. Biến counter là 1 phần của vùng global,
hay thử console.log(counter) và bạn sẽ nhận được undefined. Vì vậy , đây không phải là nguyên nhân.
Vậy chắc chắn có một cơ chế khác ở đây. Vâng đó chính là closure...
Đây là cách nó làm việc. Mỗi khi ban khai báo một biến function mới và gán nó cho một biến, bạn lưu trữ function đó trong 1 biến. Biến này chính là một closure.
Biến closure này chứa tất cả các biến đã được khai báo có trong phạm vi của function đó, trong khoảng thời gian tạo function đó. Nó giống như 1 cái balo.
Lần này hãy thử giải thích lại đoạn code trên
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 1 biến mới là biến createCounter trong vùng global và gán giá trị cho nó là 1 function.
2. Dòng 9. Định nghĩa biến increment trong vùng global.
3. Thực thi function createCounter và gán giá trị trả về cho biến increment.
4. Dòng 1 - 8. Gọi function. Tạo vùng createCounter.
5. Dòng 2. Trong vùng createCounter, khai báo một biến mới là counter, và gán giá trị 0 cho nó.
6. Từ dòng 3 - 6. Khai báo một biến mới là myFunction. Biến này được khai báo trong vùng createAdder.
Giá trị được gán cho biến là một function. Như định nghĩa trong dòng 4 dòng 5. Bây giờ, chúng ta cũng tạo một closure và bao hàm nó như một phần của việc khai báo function.
Closures chứa tất cả các biến trong phạm vi khi function được khai báo, trong trường hợp này khi myFunction được khởi tạo thì từ dòng 4 - 5 tồn tại biến counter có giá trị 0.
7.Dòng 7 trả về nội dung của biến myFunction. vùng createCounter bị phá hủy
Các biến myFunction và counter không tồn tại nữa.Nhưng chúng ta không chỉ trả về nội dùng của biến myFunction mà còn trả về closure của nó nữa - 1 chiếc ba lô với tất cả các biến có trong phạm vi khi nó được tạo (counter = 0)
8. Dòng 9, giá trị được trả về bởi createCounter được gán cho increment.
Biến increment giờ chứa 1 function và ba lô closure. Một định nghĩa function đc trả về bởi createCounter nhưng nó không có nhãn là myFunction nữa mà là increment.
9. Dòng 10, Khai báo biến c1.
10, Dòng 10, tìm biến increment và thực thi nó trong vùng global. Nó là một function được trả về và kèm theo nó là 1 balo closure với các biến (counter = 0)
11. Tạo vùng increment, không có tham số truyền vào.. => sẽ bắt đầu từ thực thi function.
12. Dòng 4. counter = counter + 1. Chúng ta cần tìm biến counter. Trước khi tìm trong vùng local và vùng global. JS sẽ tìm trong cái balo trược.
Tất nhiên nó sẽ thấy biến counter đang bằng 0, sau đó nó sẽ tìm ở vùng local và global , nếu tìm thấy 1 biến counter nó sẽ ghi đè lên giá trị được tìm thấy trong ba lo. Tất nhiên trong trường hợp này nó không xảy ra.
NHư vậy, biến counter sẽ bằng kết quả của phép cộng 0 + 1 = 1. Và nó được lưu trữ trong ba lô closure. Lúc nào trong ba lo có biến counter với giá trị là 1.
13. Dòng 5, Chúng ta trả về nội dung biến counter hoặc số 1. Chúng ta phá hủy vùng cục bộ.
14. Trở về dong 10. Giá trị trả về là 1 và được gán cho c1.
15. Dòng 11. lặp lại các bước từ 10 đến 14. Hãy lưu ý lúc nào trong ba lô có biến counter với giá trị là 1.
16. DÒng 12.
17. 1, 2, 3. Sẽ đc in ra trong màn hình log
TLDR;
Tóm tắt lại keyword để nhớ về closure đó là khi một function được khai báo thì nó chứa 1 code nội dung của function đó và 1 closure.
Closure là 1 một tập hợp các biến trong 1 phạm vi của function tại thời điểm function đó được khởi tạo
Bạn sẽ thắc mắc, function nào cũng sẽ có closure, kể cả những function được tạo trong vùng global.
Câu trả lời là có. Cái túi ba lô của các function được tạo trong vùng toàn câu, chính là các biến global.
Qua đây hãy thực hành giải thích cách code in ra console số 7 :
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)
Kết luận
Mình luôn nhớ ra closure là 1 cái túi chứa các biến tồn tại và có giá trị ảnh hưởng đến vùng của 1 function khi function đó được tạo.
Cảm ơn các bạn đã đọc bài viết này ^.^