[JavaScript] Tìm hiểu về JavaScipt Closures
Chúng ta thường xuyên sử dụng closures trong JavaScript, kinh nghiệm về JavaScript của bạn không quan trọng, chắc chắn bạn sẽ bắt gặp chúng hết lần này đến lần khác. Closures có thể khá phức tạp và nằm ngoài khả năng của bạn, nhưng sau khi đọc bài viết này, closures sẽ trở lên dễ hiểu hơn và bạn có thể sử dụng chúng cho các task JavaScript hàng ngày của mình.Bạn nên hiểu rõ scope (phạm vi) của biến trong JavaScript trước khi đọc tiếp, bởi vì để hiểu closures bạn phải hiểu scope của biến trong JavaScript.
JavaScript Closures luôn là một thứ gì đó rất huyền bí. 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à mình không hề biết mình đang sử dụng nó.
Một số ví dụ đơn giản
Trước khi bắt đầu, chúng ta hãy tìm hiểu những ví dụ đơn giản sau đây:
Ví dụ 1
Đối với bất kỳ ai đã từng làm việc với JavaScript thì kết quả đoạn code trên rất rõ ràng. Đoạn code trên sẽ in ra màn hình kết quả 12. Để hiểu được cách JavaScript engine nó hoạt động như thế nào, chúng ta hãy break down chi tiết đoạn code trên:
- Dòng 1, khai báo một biến mới có tên là val1 trong global execution context, sau đó gán giá trị cho nó là 2.
- Dòng 2-5, khai báo là một biến tên là multiplyThis, sau đó gán cho nó giá trị là định nghĩa của một function.
- Dòng 6, khai báo một biến mới có tên là multiplied trong global execution context. Sau đó gán giá trị của multiplyThis và thực thi nó với tham số truyền vào là 6.
- Quay lại dòng 2, thực thi function đồng nghĩa với việc một local execution context mới được tạo ra.
- Trong local execution context này, có một điểm chú ý là biến được tạo đầu tiên không phải là ret mà là biến n. Sau đó, gán giá trị của tham số truyền vào cho n => n=6.
- Dòng 3, khai báo một biến mới tên là ret.
- Vẫn trên dòng 3, thực hiện phép nhân với hai toán tử là giá trị của hai biến n và val1. Trong local execution context, tìm biến n. Nó lã được gắn ở bước 5 và giá trị của nó ở đây là 6. Tiếp đến tìm biến val1. Trong local execution context hiện tại không có biến nào tên val1. Tìm đến calling context. calling context lúc này là global execution context. Ơn giờ, ở bước 1 ta đã defined một biến val1 và giá trị của nó là 2.
- Thực hiện phép nhân giữa hai biến n và val1 => 6x2=12. Sau đó, gán giá trị cho biến ret => ret = 12.
- Dòng 4, return giá trị của biến ret. Tiến hành pop local execution context ra khỏi Call Stack, đồng thời xóa biến ret và n khỏi bộ nhớ. Ở đây, biến val1 không bị hủy vì nó nằm trong global execution context.
- Quay là dòng 6, trong calling context, biến multiplied được gán giá trị là 12.
- Cuối cùng, trên dòng 7, chúng ta sẽ thấy giá trị 12 được in 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 có thể truy cập đến những biến trong local execution context của nó thì nó có thể truy cập đến những biến trong calling context gọi đến function đó.
Ví dụ 2
Ở ví dụ này, chúng ta sẽ tìm hiểu một function có giá trị trả về là một function.
- Tương tự như ở ví dụ trên, dòng 1-8, khai báo biến val có giá trị là 7 và biến createAdder có giá trị là định nghĩa của một function.
- Dòng 9, khai báo một biến adder, gán giá trị của createAdder và thực thi nó. Ở đây, biến adder nhận giá trị là định nghĩa của function addNumbers được trả về trong function createAdder.
- Dòng 10, khai báo biến sum, gán giá trị của adder cho nó. Ở đây, adder đang nhận giá trị là một function nên sau đó ta thực thi function này với tham số truyền vào là val và 8.
- Dòng 3, khai báo hai biến a và b nhận giá trị là giá trị của tham số truyền vào. Biến a nhận giá trị của tham số truyền vào đầu tiên, ở đây là giá trị của biến val => a=7. Biến b nhận giá trị của tham số truyền vào thứ 2 => b=8.
- Dòng 4, khai báo biến ret, gán giá trị của phép cộng giữa a và b => ret=7+8=15. Trả về giá trị của biến ret là 15.
- Dòng 10, tiến hành pop addNumbers khỏi Call Stack, đồng thời xóa biến a, b và ret khỏi bộ nhớ. Biến sum được gán giá trị là 15.
- Cuối cùng, in giá trị 15 ra màn hình.
Qua hai ví dụ đơn giản phía trên, chúng ta cần lưu ý một số điểm quan trọng.
- Định nghĩa của một function có thể lưu vào một biến, function đó có thể không xuất hiện trong chương trình cho đến khi nó được gọi đến.
- Mỗi lần function được gọi, một local execution context sẽ được tạo. Execution context đó sẽ bị pop khỏi Call Stack khi một function kết thúc.
Closure
Tiếp theo, chúng ta hãy nhìn đoạn code phía dưới và tìm hiểu xem kết quả sẽ là gì?
Theo bạn, kết quả in ra màn hình sẽ là gì? A. 1 1 1 B. 1 undefined undefined C. undefined undefined undefined D. 1 2 3 E. Một đáp án khác Nếu dựa vào cách giải nghĩa từ hai ví dụ trên thì đáng lẽ ra kết quả sẽ là đáp án A đúng không? Nhưng có gì đó không ổn ở đây, khi execute đoạn code trên thì kết quả khi in ra màn hình không phải là 1 1 1 mà lại là 1 2 3. How? Bằng một cách thần kỳ nào đó, mà JavaScript đã nhớ được giá trị của biến counter.
Liệu có phải biến couter là một phần của global execution context không? Thử console.log(counter) xem sao và thật là giá trị lại là undefined.
Có lẽ, khi gọi function increment, bằng cách nào đó nó quay lại function đã tạo trước đó createCounter? Nhưng giá trị biến increment là một function definition nên không thể thực hiện điều này.
Vậy thì phải có một cơ chế khác ở đây. Đó chình là Closure.
Closure chứa tất cả các biến mà trong scope tại thời điểm tạo function đó. Nó giống như một chiếc ba-lô chứa tất cả các biến trong scope tại thời điểm function được tạo ra.
Vì vậy, ví dụ trên có thể được giải thích như sau:
- Dòng 1-8, khai báo biến createCounter có giá trị là định nghĩa của một function.
- Dòng 9, khái báo biến increment, gán giá trị của createCounter và thực thi nó. Ở đây biến increment nhận giá trị là định nghĩa của function myFunction.
- Dòng 10, khai báo biến c1, gán giá trị của biến increment và thực thi nó.
- Dòng 4, tìm biến counter. Trước khi tìm trong local execution context hoặc global execution context, chúng ta check trong closure. Trong Closure, chúng ta có chứa biến có tên là counter, nó có giá trị là 0.
- Sau đó, thực hiện phép tính, kết quả trả về là 1. Closure bây giờ chứa biến counter có giá trị mới là 1.
- Trả về giá trị của biến counter là 1.
- Quay lại dòng 10, gán giá trị 1 cho biến c1.
- Dòng 11, lặp lại các bước 4-7. Lúc này tìm trong closure, ta thấy biến counter có giá trị là 1. Ở bước 5, biến counter được gán giá trị là 2. Và biến c2 có giá trị là 2.
- Dòng 12, tương tụ, biến counter có giá trị là 2. Và biến c3 có giá trị là 3.
- Cuối cùng, in ra màn hình lần lượt giá trị của các biến c1 c2 c3 là 1 2 3.
Qua ví dụ trên ta thấy đôi khi Closure xuất hiện nhưng chúng ta không hề nhận ra nó.
##For fun - Sử dụng Closure để implement useState() của React hooks
function useState(initVal) {
let _val = initVal;
const state = () => _val;
const setState = newVal => {
_val = newVal;
}
return [state, setState];
}
const [count, setCount] = useState(1);
console.log(count());
setCount(2);
console.log(count());
Hãy thử execute đoạn code phía trên và theo dõi kết quả ^^
Kết luận
Để đơn giản, hãy tưởng tượng closure tương tự như cái ba-lô. Nó chứa tất cả các biên trong scope từ lúc function được khai báo. Ngoài ra, closures còn giúp cho JavaScipt có thể define được private variables.
[Closure] make it possible for a function to have "private" variables.
-W3Schools-
Tài liệu tham khảo
Theo dõi VnCoder trên Facebook, để cập nhật những bài viết, tin tức và khoá học mới nhất!