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:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token keyword">function</span> <span class="token function">say</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> term <span class="token operator">=</span> <span class="token string">"I would like to say:"</span><span class="token punctuation">;</span> <span class="token keyword">function</span> <span class="token function">stuff</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>term<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> Hello Hi Ha Ya</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> stuff<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">let</span> s <span class="token operator">=</span> <span class="token function">say</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">s</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">></span> <span class="token constant">I</span> would like to say<span class="token operator">:</span> 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:
1 2 3 4 5 6 7 8 9 10 11 | <span class="token keyword">function</span> <span class="token function">wait</span><span class="token punctuation">(</span><span class="token parameter">message</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">setTimeout</span><span class="token punctuation">(</span> <span class="token keyword">function</span> <span class="token function">timer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span> message <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">1000</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token function">wait</span><span class="token punctuation">(</span> <span class="token string">"Hello, closure!"</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token operator">></span> Hello<span class="token punctuation">,</span> closure<span class="token operator">!</span> |
Đâ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ì ?
1 2 3 4 5 6 | <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">var</span> i<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">;</span> i<span class="token operator"><=</span><span class="token number">5</span><span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">setTimeout</span><span class="token punctuation">(</span> <span class="token keyword">function</span> <span class="token function">timer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span> i <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> i<span class="token operator">*</span><span class="token number">1000</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
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à i
mà setTimeout
nhận được đang khác với i
mà timer
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.
1 2 3 4 5 6 7 8 9 | <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">var</span> i<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">;</span> i<span class="token operator"><=</span><span class="token number">5</span><span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token keyword">var</span> j <span class="token operator">=</span> i<span class="token punctuation">;</span> <span class="token function">setTimeout</span><span class="token punctuation">(</span> <span class="token keyword">function</span> <span class="token function">timer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span> j <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> j<span class="token operator">*</span><span class="token number">1000</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Hoặc
1 2 3 4 5 6 7 8 | <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">var</span> i<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">;</span> i<span class="token operator"><=</span><span class="token number">5</span><span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">j</span><span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token function">setTimeout</span><span class="token punctuation">(</span> <span class="token keyword">function</span> <span class="token function">timer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span> j <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> j<span class="token operator">*</span><span class="token number">1000</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span> i <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Cú pháp này:
1 2 3 4 | (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 debounce
và throttle
Đó 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 và đây .
Một điều thú vị nữa với ví dụ ở trên
1 2 3 4 5 6 | <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">var</span> i<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">;</span> i<span class="token operator"><=</span><span class="token number">5</span><span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">setTimeout</span><span class="token punctuation">(</span> <span class="token keyword">function</span> <span class="token function">timer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span> i <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> i<span class="token operator">*</span><span class="token number">1000</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
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.