Closure trong Javascript - Phần 2: Định nghĩa và cách dùng

Các bạn có thể đọc qua phần 1 ở đây

Để mọi người không quên, mình xin tóm tắt gọn lại khái niệm lexical environment:

    Lexical Environment là một object giấu tên có trong mọi object trong Javacript, nó chứa các biến trong 1 scope và các reference đến môi trường bên ngoài.

Oke chứ ? Giờ thì đến định nghĩa về Closure:

    Closure là một hàm mà có thể nhớ và truy cập lexical environment của nó ngay cả khi ở ngoài lexical environment đó.

    Chúng ta hãy cùng xem ví dụ sau:

function say() {
    let term = "I would like to say:";
    
    function stuff() {
        console.log(`${term} Hello Hi Ha Ya`);
    }
    
    return stuff;
}
let s = say();
s()
> I would like to say: Hello Hi Ha Ya

    Có thể thấy hàm stuff được trả về bởi hàm say, và chúng ta gán s thành chính hàm stuff (chứ không phải giá trị trả về của hàm stuff).

    Vậy, hàm stuff sau khi được gán vào s, đã không còn chạy trong lexical environment mà nó được khai báo kèm.

    Sau khi hàm say được chạy, chúng ta sẽ nghĩ rằng các thông tin trong lexical environment của nó sẽ biến mất, nhưng bởi vì hàm stuff vẫn tồn tại và có chứa một kết nối đến lexical environment bên ngoài (ở đây là hàm say, các thông tin này vẫn được lưu giữ. Có người gọi chính sợi dây kết nối này mới là closure.

Closure trong Javascript thông dụng hơn bạn nghĩ

    Chúng ta sẽ lấy tiếp 1 ví dụ như sau:

function wait(message) {

	setTimeout( function timer(){
		console.log( message );
	}, 1000 );

}

wait( "Hello, closure!" );
> Hello, closure!

    Đây là một ví dụ lấy trực tiếp từ sách You Dont Know Javascript . Ai đã từng động đến Javascript thì đều đã dùng đến hàm setTimeout. Ở trong ví dụ này, chúng ta có một hàm trong cùng là timer và sẽ truyền nó vào trong setTimeout để chạy sau 1 giây. Đây là một cách dùng mà mình nghĩ là khá phổ biến. Cũng giống như ví dụ trước, có thể bạn nghĩ rằng sau 1s thì tham số message của hàm wait đúng ra đã phải biến mất, nhưng hàm timer ở đây vẫn có một sợi dây kết nối đến lexical environment của wait, và ngăn không cho message bị bộ dọn rác (Garbage Collector) xóa.

Closure và vòng lặp

    Bạn nghĩ vòng lặp sau sẽ in ra gì ?

for (var i=1; i<=5; i++) {
	setTimeout( function timer(){
		console.log( i );
	}, i*1000 );
}

    Kết quả thực là nó sẽ in ra số 6 5 lần, mỗi lần cách 1 giây. Tức là isetTimeout nhận được đang khác với itimer nhận được, tại sao thế ?

    Đầu tiên, 6 đến từ đâu ? Nó là kết quả khi vòng lặp gặp điều kiện không thỏa mãn i <= 5, tức i = 6, khi đó nó kết thúc và hàm setTimeout mới bắt đầu được chạy. Và cho dùng bạn có thay i*1000 bằng 0, hàm setTimeout vẫn sẽ chạy sau vòng lặp hoàn thành. Vậy làm thế nào để in đúng như mình muốn ?

    Điều chúng ta muốn ở đây có lẽ là mỗi vòng lặp bắt được biến i của riêng nó, nhưng hiện tại, cả 5 lần lặp của vòng lặp này đang dùng chung một lexical environment bên ngoài mà chỉ có một i khi vòng lặp đã chạy xong. Vậy cái chúng ta cần là một lexical environment mà có thể chứa biến i này.

for (var i=1; i<=5; i++) {
	(function(){
		var j = i;
		setTimeout( function timer(){
			console.log( j );
		}, j*1000 );
	})();
}

    Hoặc

for (var i=1; i<=5; i++) {
	(function(j){
		setTimeout( function timer(){
			console.log( j );
		}, j*1000 );
	})( i );
}

    Cú pháp này:

(function(){
   ...
})();

    Là một IIFE , việc dùng một IIFE trong vòng lặp sẽ tạo ra một lexical environment mới bọc quanh hàm setTimeout ở mỗi vòng, cung cấp cho nó biến i mà nó cần.

Closure được dùng trong trường hợp nào ?

    Có nhiều tính huống đời thường dùng đến closure, bạn hãy thử đọc lại Javascript mình đã viết xem có chỗ nào dùng đến không. Nhiều người sử dụng nó để viết các hàm debouncethrottle

    Đó là những điều cơ bản nhất mà mình biết về Closure. Các bạn có thể tham khảo thêm ở đâyđây . Một điều thú vị nữa với ví dụ ở trên

for (var i=1; i<=5; i++) {
	setTimeout( function timer(){
		console.log( i );
	}, i*1000 );
}

    Là khi bạn thay var bằng let, hành vi khác lại xảy ra. Bạn thử tự tìm hiểu xem tại sao lại thế nhé.

    Cảm ơn vì đã đọc.

Nguồn: Viblo

Bình luận
Vui lòng đăng nhập để bình luận
Một số bài viết liên quan