understand Closure in Javascript (p2)

Tl;dr

(Xem phần trước của bài viết understand Closure in Javascript (p1))

Excution Contexts và Script Chains

(tiếp tục)

Scope Chains và [[scope]] property

Nhắc sơ lại rằng Javascript function luôn nằm trong một outer scope và bên trong chúng có một internal scope. Khi function object được tạo ra, nó là một phần của outer scope, và chỉ khi function được gọi thực thi (call) thì intenal scope (và execution context ) của nó mới được sinh ra. Ngoài ra Javascript standard còn cung cấp cho function object một property có tên là [[scope]], property này sẽ tham chiếu đến outer scope của function

Ngay từ ban đầu trước khi browser chạy script file, nó sẽ lướt qua toàn bộ mã nguồn và tiến hành giai đoạn thứ nhất (Creation Phase). Ở giai đoạn này, global scope tự động được mở ra, đi kèm với sự hình thành của global execution context. Bên trong global execution context là thực thể global scope có dạng như một list (hay chain) chứa tất cả object nằm trong phạm vi toàn cục (global). Đặc biệt global object nằm ở đầu list. Các function được tạo ra thuộc phạm vi toàn cục (global) thì [[scope]] property của chúng sẽ tham chiếu trực tiếp đến thực thể global scope

Xem lại toàn bộ đoạn code ở bài viết trước:

// global scope
var glVar1;
var glVar2;

function MyFunction(){
    var localVar1;
    var localVar2;
    function innerOfMyFunction(){
        // .... some code here ....
    }
}

function b(){
     // some code here….
}

var c = Function c(){
     // some code here….
}


// .......

MyFunction();   // call MyFunction

Tổng quan ta có
alt text

Ta cùng chuyển sự chú ý về [[scope]] property trong function object, property này đặt biệt ở chỗ, khi function object được tạo trong một outer scope nào thì nó sẽ tham chiếu đến outer scope đó.
MyFunction’s function object được tạo ra trong global scope nên [[scope]] property tham chiếu đến global scope
Thế innerOfMyFunction’s function object sẽ có [[scope]] property tham chiếu đến đâu?

Trong hình minh họa trên ta thấy innerOfMyFunction’s [[scope]] property trỏ đến cả MyFunction’s scopeglobal scope. Mình họa như vậy để cho ta thấy được rằng outer scope của innerOfMyFunction là có sự kết hợp giữa MyFunction’s scopeglobal scope thôi chứ không phải nó tham chiếu một lúc đến hai nơi như vậy đâu. [[scope]] property cũng chỉ là một biến tham chiêu thôi mà, làm sao một biến có tham chiếu đến hai nơi được. Lúc này ta cần đến Scope chain, vậy Scope chain do đâu mà ra? Hãy thử ngẫm nghĩ một lần nữa xem, tại sao Javascript standard lại cung cấp cho ta [[scope]] property để làm cái nồi gì?

Chính nó đấy, chính [[scope]] property của MyFunction là thứ cầu nối giữa global execution context và MyFunction’s execution context hay nói cách khác:
Scope chain là sự nối liền MyFunction’s scope với global scope, tạo thành một list (hay chain) chứa rất nhiều objects nằm bên trong, bao gồm Activation object và global object (Lưu ý là MyFunction’s scope sẽ liền trước global object).
Và innerOfMyFunction’s [[scope]] property sẽ tham chiếu đến Scope chain đó

Dài quá ta phải rút gọn cái scope chain lại!
Theo như jibbering, thì global scope chỉ nên biểu diễn mỗi phần tử global object thôi. Thật vậy, vì global object đã tham chiếu đến các phần tử còn lại thông qua các properties cùng tên, nên ta biểu diễn thêm chúng chỉ bằng thừa. Ta cũng thấy điều tương tự diễn ra đối với Activation object trong MyFunction’s scope

Okay thế tuyệt quá rồi Scope chain mà innerOfMyFunction’s [[scope]] property tham chiếu tới sẽ có cấu trúc như vầy:
alt text

Lưu ý: mặc dù mình tách innerOfMyFunction’s function object ra thành một ellipse độc lập để cho ta dễ dàng nhìn thấy [[scope]] propery tham chiếu đến Scope chain. Thực chất innerOfMyFunction’s function object vẫn là một phần của MyFunction’s scope

Identifier Resolution

var glVar1 = 1;
var glVar2 = 2;
var author = {
     name: “Trung”,
     describe: “kind”
};

function MyFunction(){
     var localVar1 = 3;
     var localVar2 = 4;

     function innerOfMyFunction(){
          console.log(glVar2);         // glVar2 lấy từ đâu
          console.log(localVar1);          // localVar1 lấy từ đâu
          console.log(localVar2);          // localVar2 lấy từ đâu
     }

      console.log(glVar1);  // glVar1 lấy từ đâu

      innerOfMyFunction();

      console.log(author.name + “ is ” + author.describe);
}


MyFunction();


// kết quả:
// 1
// 2
// 3
// 4
// Trung is kind

Các biến trên lấy từ đâu và khi thực thi các lệnh trên thì hệ thống duyệt xuyên suốt Scope chain như thế nào?

Ta phải chú ý một chút đến các thời điểm trong quá trình chạy script file:
(Creation Phase) bắt đầu ---> global execution context được tạo ra (bao gồm sự hình thành MyFunction’s function object)---> MyFunction’s [[scope]] property tham chiếu đến global scope của global execution context ---> (Execution Phase) bắt đầu ---> chạy line-by-line các dòng script code ---> bắt gặp MyFunction được gọi thực thi ---> MyFunction’s execution context được tạo ra (bao gồm cả sự hình thành của innerOfMyFunction’s function object) ---> xuất hiện Scope chain liên kết MyFunction’s scopeglobal scope ---> innerOfMyFunction’s [[scope]] property tham chiếu đến Scope chain ---> chạy line-by-line các dòng script code bên trong MyFunction’s function body ---> bắt gặp lệnh console.log(glVar1); ---> bắt gặp innerOfMyFunction được gọi thực thi ---> innerOfMyFunction’s execution context được tạo ra ---> xuất hiện Scope chain mới là sự liên kết giữ Scoper chain cũ với innerOfMyFunction’s scope (tất nhiên innerOfMyFunction’s scope phải nằm liên trước Scope chain cũ) ---> băt gặp lệnh console.log(glVar2);

Khi console.log(glVar1); được gọi thì Scope chain đã được tạo ra từ lâu, hệ thống sẽ tìm đến phần tử đầu tiên của scope chain chính là Activation object, và tìm xem nó có chứa property nào tên là glVar1 không, nếu không có thì sẽ chuyển sang phần tử thứ hai đó là global object. Thật may mắn lần này hệ thống tìm được glVar1 = 1 trong global object và thực hiện lệnh console.log() thành công!
Tương tự innerOfMyFunction(); được gọi, vẫn bắt đầu tự phần tử đầu tiên của Scope chain. Lại may mắn làm sao Activation object có property tên là innerOfMyFunction. Vậy là yên tâm để bước vào innerOfMyFunction’s function body để chạy (hệ thống ưu tiên inner function chạy trước theo cấu trúc Stack).

Khi console.log(glVar2); được gọi thì Scope chain mới đã được tạo ra từ lâu, hệ thống lại tìm đến phần tử đầu tiên của scope chain chính là Activation object của innerOfMyFunction’s scope và tất nhiên là nó chẳng có property nào tên glVar2. Hệ thống chuyển sang phần tử thứ hai đó là Activation object của MyFunction’s scope mà cũng chẳng thấy đâu. Nhưng đến phần tử cuối cùng là global object thì có glVar2 = 2. Lần này lại chạy thành công!

…. Cứ thế mà tiếp tục thì ta sẽ có được kết quả như trên.

Có một điều đặc biệt đáng chú ý ở đây là có sự tham gia của object author , chắc chắn hệ thống sẽ tìm kiếm author.name trong Property chain. Bạn đã thấy được liên quan giữa Scope chainProperty chain chưa?

Closures

Cuối cùng đến phần chính. Mình đã giải thích tại sao lại có mặt Closure ở (p1)
Sẽ là điều bình thường khi kết thúc một function thì execution context của nó cũng mất theo. Tất cả mọi thứ từ đầu đến giờ, Scope chain, Activation/Variable object, các local variable, các local function object,…. sẽ bị garbage collection dọn sạch. Nhưng Javascript còn hơn thế, xem function như f*irst-class object, cho phép trả về một function thông qua lệnh return đã mang đến cho ta **Closure. Đối với Douglas Crockford, **Closure* là thứ gì đó rất hay ho đến từ Javascript mà trước nó, chưa có ngôn ngữ lập trình nào thật sự triển khai và áp dụng.

Forming Closure

function exampleClosureForm(arg1, arg2){
     var localVar = 8;
     function exampleReturned(innerArg){
          return ((arg1 + arg2) / (innerArg + localVar));
     }

      /* return a reference to the inner function defined as 
      exampleReturned
      */
      Return exampleReturned;
}

var globalVar = exampleClosureForm(2, 4);

function object sau khi được trả về từ lần gọi hàm exampleClusureForm(2, 4) nhất định sẽ không bị giải phóng bởi garbage collection bởi vì nó được tham chiếu bởi một global variable tên globalVar, tất nhiên globalVar là exampleReturned’s function object, nên ta có thể gọi thực thi nó ở lần sau.

Nhưng mọi chuyện có cái gì đó rất phức tạp, bởi vì function object được trả về và được tham chiếu bởi globalVar, nhưng trước đó nó đã được sinh ra trong exampleClosureForm’s scope. Điều đó đồng nghĩa với việc [[scope]] property của nó vẫn còn đang tham chiếu tới Scope chain (gồm các phần tử: Activation/Variable object của exampleClosureForm’s execution context, liền sau là global object). Như vậy thì các phần tử trong Scope chain sẽ không bị garbage collection giải phóng

Closure đã được hình thành từ đó. exampleReturned’s function object sau khi được trả về có thể thoải mái gọi thực thi bất kì lúc nào và thoải mái sử các parameter, local variable hay các inner function khác,… bên trong exampleClosureForm(). Vì giờ đây tất cả đã được gói gọn trong Scope chain tham chiếu bởi [[scope]] property.

Nhưng để cho chắc ăn, ngoài ra để ôn lại những gì đã biết. Hãy cùng thử hình dung những gì sẽ xảy ra:

function exampleClosureForm(arg1, arg2){
     var localVar = 8;
     function exampleReturned(innerArg){
          return ((arg1 + arg2) / (innerArg + localVar));
     }

      /* return a reference to the inner function defined as 
      exampleReturned
      */
      Return exampleReturned;
}

var globalVar = exampleClosureForm(2, 4);

globalVar(2);   // call

Ta lại cùng điểm qua các giai đoạn:
(Creation Phase) bắt đầu ---> global execution context được tạo ra (bao gồm sự hình thành exampleClosureForm’s function object)---> exampleClosureForm’s [[scope]] property tham chiếu đến global scope của global execution context ---> (Execution Phase) bắt đầu ---> chạy line-by-line các dòng script code ---> bắt gặp exampleClosureForm được gọi thực thi ---> exampleClosureForm’s execution context được tạo ra (bao gồm cả sự hình thành của Action/Variable object và exampleReturned’s function object) ---> xuất hiện Scope chain liên kết exampleClosureForm’s scopeglobal scope ---> exampleReturned’s [[scope]] property tham chiếu đến Scope chain ---> chạy line-by-line các dòng script code bên trong exampleClosureForm’s function body ---> bắt gặp lệnh return exampleReturned; ---> kết thúc exampleClosureForm ---> tiếp tục các dòng script code bên ngoài global scope ---> bắt gặp exampleRetuerned's function object được tham chiếu với globalVar bị gọi thực thi ---> exampleReturned’s execution context được tạo ra (bao gồm sự hình thành của Activation/Variable object bên trong exampleReturned’s scope) ---> hình thành Scope chain mới là sự liên kết giữa exampleReturned’s scopeScope chain cũ (tham chiếu bởi [[scope]] property) ---> ….

Scope chain mới sẽ gồm các phần tử theo thứ tự: Activation/Variable object của exampleReturned’s scope --> Activation/Variable object của exampleClosureForm’s scope --> global object

return ((arg1 + arg2) / (innerArg + localVar));

Để thực thi lệnh trên, hệ thống sẽ duyệt theo Scope chain mới bắt đầu từ phần tử đầu tiên
Hệ thống sẽ tìm thấy:

  • arg1, arg2 và localVar thuộc Activation/Variable object của exampleClosureForm’s scope
  • innerArg thuộc Activation/Variable object của exampleReturned’s scope

Ta áp dụng Closures như thế nào?

(sẽ tìm hiểu tiếp ở p3)

Referrences

(tham khảo từ bài viết Javascript Closure - FAQ notes)
(xem thêm Javascript Closure & scope chain with example)
(xem thêm Javascript the good parts)
(xem thêm YDLJS - Scope & Closures)

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Đức Trung

3 bài viết.
12 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
2 0
Tl;dr Tiếp tục series tìm hiểu về javascript Giới thiệu viết lại nguyên văn: Closure A "closure" is an expression (typically a function) tha...
Đức Trung viết 5 tháng trước
2 0
White
2 1
Tl;dr lưu ý một chút về cách trình bày: .propname tức là public property có tên là "propname" của một đối tượng Bắt đầu với Object, Function v...
Đức Trung viết 5 tháng trước
2 1
Bài viết liên quan
White
40 11
Scope = cửa hậu aka lỗ đen. Black holes are where God divided by zero Albert Einstein (Ảnh) Chúng ta là những lập trình viên thiên tài, chúng ta...
quocnguyen viết hơn 2 năm trước
40 11
White
45 8
Tăng sức mạnh cho javascript với lodash Lần này mình sẽ giới thiệu 1 thư viện javascript vô cùng bá đạo có tên là "lodash]1]", có thể nói nó là LI...
Huy Hoàng Phạm viết hơn 2 năm trước
45 8
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
3 bài viết.
12 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!