Closure trong JavaScript
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:
- Khai báo function
outer
ở global memory - Dòng 11 :
const myFunction = outer()
, khi gọi functionouter
, JavaScript sẽ:- Đẩy
outer()
vào call stack - Tạo một execution context tương ứng
- Khai báo biến
counter
và gán giá trị0
cho nó ở local memory - Khai báo function
incrementCounter
ở local memory - return
incrementCounter
chomyFunction
ở global memory
- Đẩy
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().
- Lúc này thread of execution đi đến dòng 12, và thực thi:
myFunction();
- Lúc này JavaScript đẩymyFunction()
vào call stack và tạo một execution context tương ứng cho nó.
- TrongmyFunction
, ta thực thicounter++
, nhưng tìmcounter
ở đâu?- Đầu tiên là tìm
counter
ở local memory. Không thấy - Chúng ta thực thi
myFunction
ở đâu? Global! Vậy tìmcounter
ở global memory, nhưng cócounter
nào ở global memory đâu?
- Đầu tiên là tìm
Để 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()
.
- 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.
- 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 thiouter
.
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, counter
ở backpack này giá trị là 0
.
- 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ìmcounter
tại backpack ởmyNewfunction
và nâng giá trị củacounter
tại đây lên1
.
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