Closure trong Javascript
Closure là hàm có khả năng nhớ và truy cập lexical scope của nó ngay cả khi hàm đó được thực thi bên ngoài lexical scope của nó. Xem vài ví dụ minh hoạ:
Đoạn code nhìn thương tự như nested scope. Hàm bar() truy cập đến biến a trong phạm vi bao ngoài vì quy tắc tìm kiếm lexical scope (trường hợp này là tìm kiếm RHS).
Đây có phải là 'closure' ?
Về kỹ thuật thì có lẽ. Nhưng những gì bạn cần biết như ở trên thì không chính xác.
Chính xác nhất để giải thích bar() tham chiếu đến a thông qua quy tắc tìm kiếm lexical scope và các quy tắc đó chỉ là một phần của closure.
Từ góc độ hàn lâm thuần tuỳ, thì bar() ở trên thì có closure trong phạm vi của foo(). Còn nói cách khác thì bar() đóng kín trong phạm vi của foo(). Vì sao ? Đơn giản vì bar() xuất hiện lồng bên trong foo() :'(.
Nhưng xác định closure theo cách này không trực tiếp observable(quan sát được) và ta không thấy được closure được thể hiện gì trên đoạn code trên cả. Ta thấy rõ lexical scope nhưng không thấy closure ẩn sâu bên trong.
Hãy xem đoạn code dưới đây để thấy closure ra ngoài ánh sáng:
Hàm bar() có lexical scope truy cập vào scope của foo(), sau đó ta lấy chính bar() và truyền nó như giá trị. Trường hợp này return bar như một tham chiếu. Sau khi thực thi foo() ta gán giá trị trả về (hàm bar() và bên trong) cho biến baz. Sau đó ta gọi hàm baz() tức là gọi bar().
Chắc chắn bar() được thực thi. Nhưng nó thực thi bên ngoài lexical scope nó khai báo.
Sau khi foo() thực thi chúng ta cho rằng toàn bộ scope bên trong foo() sẽ ra đi vì Engine sử dụng công cụ gom rác và giải phóng bộ nhớ. Và dường như nội dung foo() không còn được sử dụng và như biến mất.
Nhưng điều kỳ diệu của closure đã không để điều này xảy ra. Nghĩa là scope bên trong foo() vẫn đang được sử dụng và bar() đang sử dụng chúng.
Vì bar() đã được khai báo nên nó có lexical scope closure qua phạm vi bên trong của foo(), việc này đã giúp cho bar() tồn tại trong việc tham chiếu về sau.
bar() vẫn có một tham chiếu đến phạm vi, và điều này được gọi là closure.
Vì vậy, vài micro giây sau đó, khi biến baz được gọi (gọi hàm bar bên trong), nó có quyền truy cập đến author-time của lexical scope, nên nó có thể tiếp cận biến a như ta mong muốn.
Hàm được gọi ngon lành cành đào từ bên ngoài của author-time lexical scope. Closure cho phép hàm tiếp tục truy cập lexical scope đã xác định tại author-time.
Ghi chú: Chúng ta đã biết IIFE, có người bảo IIFE là ví dụ cho closure nhưng tôi cho rằng nó chưa đúng.
Code này hoạt động nhưng nó không phải observation của closure. Vì hàm IIFE không được thực thi bên ngoài lexical scope.
Vòng lặp + Closure
Ví dụ điển hình cho minh hoạ closure là vòng lặp for
Với đoạn code trên ta mong muốn sẽ hiển thị ra '1,2,3,4,5' nhưng khi chạy đoạn code ta sẽ thấy in ra 6 lần lượt trong 5 lần.
Chúng ta đang cố gắng bắt một bản sao của i tại mỗi vòng lặp. Nhưng theo cách hoạt động của scope, thì ta có 5 hàm được xác định riêng biệt mỗi lần tất cả đều được đóng kín bở scope toàn cục, và chỉ có 1 i trong đó. Theo cách này thì tất cả các hàm đều có tham chiếu đến i.
Ok, vậy cái thiếu là gì ? Ta cần thêm closure scope . Đặc biệt cần closure scope cho mỗi lần lặp.
Ta đã biết tạo scope bằng IIFE tạo scope bằng cách khai báo một hàm và thực thi nó ngay lập tức.
Thử xem:
Nó có chạy không ? Không . Mặc dù có lexical scope . Mỗi hàm timeout callback đóng qua mỗi lần lặp scope của riêng nó được tạo bởi IIFE tương ứng.
Bởi vì hàm IIFE của ta là một scope rỗng chẳng làm gì cả. Nó cần có gì đó hữu dụng hơn, tức nó cần một biến riêng, là bản sao của giá trị i sau mỗi lần lặp.
OK, đã chạy, cách viết được ưa thích:
Cách dùng IIFE trong mỗi lần lặp tạo ra một scope mới cho mỗi lần lặp, cho phép hàm timeout call back cơ hội đóng thông qua scope mới cho mỗi lần lặp, và nó có biến trong mỗi lần lặp giúp ta try cập.
Vấn đề được giải quyết!