+2

Closure trong JavaScript

Mayfest2023

Nếu bạn là người mới học hoặc chưa hiểu cách thread of execution, execution context, và call stack hoạt động, bạn phải đọc bài viết này để có thể hoàn toàn hiểu khi đọc bài viết dưới đây.

Giới thiệu

Closure là khi một function "nhớ" về lexical scope của nó, ngay cả khi nó được thực thi ở bên ngoài lexical scope đó. Tạm thời cứ hiểu trừu tượng là thế, ngay dưới đây chúng ta sẽ tìm hiểu cách nó hoạt động thông qua ví dụ.

Closure

Ta sẽ tìm hiểu cách closure hoạt động, dựa trên chương trình sau:

Khi chương trình trên được chạy, JavaScript:

  1. Khai báo function outer ở global memory
  2. Dòng 11 : const myFunction = outer(), khi gọi function outer, JavaScript sẽ:
    1. Đẩy outer() vào call stack
    2. Tạo một execution context tương ứng
    3. Khai báo biến counter và gán giá trị 0 cho nó ở local memory
    4. Khai báo function incrementCounter ở local memory
    5. return incrementCounter cho myFunction ở global memory

Sau khi đã thực thi xong, function execution context đó sẽ bị xoá, ở call stack thì outer() cũng sẽ bị lấy ra, trở lại global().

  1. Lúc này thread of execution đi đến dòng 12, và thực thi: myFunction();
    - Lúc này JavaScript đẩy myFunction() vào call stack và tạo một execution context tương ứng cho nó.
    - Trong myFunction, ta thực thi counter++, nhưng tìm counter ở đâu?
    • Đầu tiên là tìm counterlocal memory. Không thấy
    • Chúng ta thực thi myFunction ở đâu? Global! Vậy tìm counterglobal memory, nhưng có counter nào ở global memory đâu?

Để biết phải tìm counter ở đâu, ta quay lại dòng 11 một xíu.

Thật ra khi dòng 11 được thực thi, ở bước cuối khi trả về (return) thì JavaScript không chỉ lưu code trong function incrementCounter vào myFunction, mà còn mang theo dữ liệu xung quanh (lexical scope) mà code trong function có tham chiếu tới. Chú ý nét vẽ màu cam nhé.

Đây là lý do ở phần giới thiệu mình có nói:

Closure là khi một function "nhớ" về lexical scope của nó, ngay cả khi nó được thực thi ở ngoài lexical scope đó.)

Vậy giờ chỉ cần đến myFunction tìm counter và thực thi dòng code là tăng giá trị của counter từ 0 thành 1.

Sau khi thực thi xong, function execution context này bị xoá (nếu local memory của nó có dữ liệu thì cũng bị xoá luôn), ở call stack thì myFunction() cũng sẽ bị lấy ra, trở lại global().

  1. Lúc này JavaScript tiếp tục thực thi dòng 13, tương tự như khi thực thi dòng 12, sau khi thực thi dòng 13 thì giá trị của counter này là 2.

Hãy tưởng tượng chúng ta có một thuộc tính ẩn [[scope]] - liên kết đến tất cả dữ liệu xung quanh nơi khai báo incrementCounter.

Và nếu những dữ liệu đó được incrementCounter tham chiếu đến, chúng sẽ đi cùng với myFunction và chúng ta chỉ có thể truy cập những dữ liệu xung quanh đã được gửi đi chung này khi thực thi myFunction. Chúng ta gọi những dữ liệu xung quanh được gửi đi chung này là backpack.

  1. Khi thực thi dòng 15, tương tự với dòng 11, JavaScript đẩy outer()vào call stack, một function execution context mới lại được tạo ra để thực thi outer.

Và vì nó hoàn toàn mới, local memory của nó cũng mới, nên khi JavaScript trả về (return) định nghĩa của function incrementCounter cho myNewFunction thì nó mang theo một backpack hoàn toàn mới, counterbackpack này giá trị là 0.

  1. Tiếp theo JavaScript thực thi dòng 16, và khi cần tìm counter để thực thi, nó sẽ về global memory và tìm counter tại backpackmyNewfunction và nâng giá trị của counter tại đây lên 1.

Sau đó thì function execution context của myNewFunction() cũng sẽ bị xoá, và myNewFunction() sẽ được lấy ra khỏi call stack, call stack lúc này trở lại global(). Và chương trình đã hoàn thành nên global execution context cũng sẽ bị xoá.

Kết bài

Như vậy là ta đã tìm hiểu cách closure hoạt động. Cũng như tại sao function có thể nhớ về những dữ liệu xung quanh của nó.

Bài này nằm trong chuỗi bài viết về JavaScript của em/ mình khi đang học, nếu có hiểu sai hay còn thiếu xót mong các bạn, anh chị góp ý!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí