understand Closure in Javascript (p1)
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) that can have free variables together with an environment that binds those variables (that "closes" the expression). Closures là một trong những ...
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) that can have free variables together with an environment that binds those variables (that "closes" the expression).
Closures là một trong những tính năng mạnh mẽ và hữu ích nhất của ECMAScript (hay JavaScript) nhưng sẽ khó có thể khai thác hết được nó nếu không thật sự hiểu về nó
Cách giải thích đơn giản sự có mặt của Closure là vì ECMAScript cho phép inner functions, function declaration và function expressions được nằm bên trong thân của một function khác (ta gọi nó là outer function). Những inner functions đó được cho phép truy cập đến tất cả local variables, parameters và những inner functions khác nằm bên trong outer function. Khi ta trả về inner function và inner function lại được sử dụng ở bên ngoài phạm vi của outer function, mà outer function thì đã kết thúc làm việc từ lâu.
function outerFunc(arg){ var localVar = 2; var innerFunc = function innerFunc(){ console.log(arg); } return function(){ innerFunc(); console.log(localVar); } } var returnedFunc = outerFunc(1); // gọi outerFunc và nhận về returnedFunc // tới đây thì chắc chắn outerFunc(1) đã kết thúc và // arg, localVar, innerFunc đều bị giải phóng khỏi bộ nhớ returnedFunc(); // chuyện gì xảy ra // kết quả in ra: // 1 // 2 // tại sao lại như vậy ???
Lúc này các inner function đó vẫn truy cập đến các variables, parameter,... của outer function. Để đảm bảo vẫn hoạt động bình thường, Closure giúp inner function ghi nhớ các variable, parameter,... của outer function mà chúng sử dụng.
Tìm hiểu thêm về function declaration và function expression) . Giải thích ngắn gọn:
- Function declaration: là cách thông thường để khai báo và định nghĩa một function.
// some code abcxyz // ….. function bar(){ return 3; }
Function declaration bị ảnh hưởng bởi cơ chế Hoisting của Javascript. Tức là đoạn code trên thực chất sẽ được biên dịch thành như sau:
function bar(){ return 3; } // some code abcxyz // ….
- Function expression: là một biểu thức nên chắc chắn có liên quan đến toán tử, ở đây toán tử đó là “=” (assign operator), có ba cách khác nhau:
// some code abcxyz // ….. // anonymous function expression var a = function(){ return 3; } // named function expression var bar = function bar(){ return 3; } // self invoking function expression (function sayHello(){ return 3; })();
Function expression sau khi thực thi sẽ được gán vào một biến, biến đó vẫn bị Hoisting nhưng bản thân function thì không. Đoạn code trên sẽ được biên dịch như sau
var a = undefined; var bar = undefined; // some code abcxyz // ….. // anonymous function expression a = function(){ return 3; } // named function expression bar = function bar(){ return 3; } // self invoking function expression (function sayHello(){ return 3; })();
Đôi chút về objects trong Javascript
(xem lại bài viết trước)
Excution Contexts và Script Chains
Đế chạy script code, Browser chia ra làm 2 giai đoạn:
giai đoạn tạo dựng (creation phase): giai đoạn này, global execution context được tạo ra bao gồm thành phần sau
this keyword lúc này tham chiếu đến global objectgiai đoạn thực thi (execution phase): khi mọi thứ đã sẵn sàng, thì việc còn lại là chạy line-by-line các dòng code
The Execution Context
Global scope tức là scope lớn nhất trong một file Javascript. Ta sẽ có một chút bối rối giữ Global scope và global execution context, chúng khác nhau như thế nào? Ta phải hiểu được rằng, Global scope chỉ là khái niệm, là cách ta quy ước về phạm vi toàn cục của một file Javascript. Còn global execution context là các thành phần phần được hệ thống (browser, server) tạo dựng lúc ban đầu, trước khi chạy một file Javascript. Ngoài ra Javascript còn có Function scope (với một số ngôn ngữ lập trình khác ta còn có Block scope).
// global scope function a(){ // function scope function b(){ //function scope } } function c(){ // function scope }
Ở hệ quy chiếu của một Javascript’s function, dù nằm ở nơi đâu, Javascript’s function vẫn được bao bọc bởi một outer scope và bản thân nó chứa một internal scope
// outer scope của function a function a(){ // internal scope của function a function b(){ // internal scope của function b } }
Nhưng khoan đã, vậy outer scope của function b ta định nghĩ như thế nào?
Outer scope của function b = global scope + internal scope của function a
Đây gọi là scope chain
Ta sẽ định nghĩa scope chain sau.
Lan man như thế vậy Execution context là gì? Và Global execution context khác gì với Execution context thông thường?
Execution context là một khái niệm trừu tượng được sử dụng bởi ECMAScript specs (ECMA-262 3rd edition) để định nghĩa cách thức làm việc của ECMAScript. Nếu Javascript Program là một State Machine thì xecution context là State còn function là Machine
Execution context Là điều kiện cần để thực thi một function
Global execution context cũng là một execution context nhưng nó tự động được tạo ra ngay từ đâu (Create Phase) . Còn execution context chỉ được tạo ra khi một function chuẩn bị thực thi và đóng lại khi function đã thực thi xong.
Khi Javascript's function được gọi ở global scope, nó mở ra một scope mới (function scope) đồng thời hình thành một execution context. Nếu một function khác (inner function) lại được gọi lồng bên trong (hoặc là gọi đệ quy của chính nó) thì một function scope mới và một execution context khác lại được tạo ra. Inner function được gọi lồng bên trong sẽ được trình thực thi ưu tiên chạy trước và kết thúc (hoặc return), lúc đó trình thực thi sẽ quay trở lại execution context của outer function. Điều đó định nghĩa được cấu trúc stack của execution context trong Javascript
function b(){ // do something.... } function a(){ myVar = 2; // do something.... b(); // call b } var myVar = 1; a(); // call a
function b đươc gọi lồng bên trong function a, cả hai đều có execution context của riêng mình, chúng cùng tạo thành một cấu trúc xếp chồng (Stack) như sau
Execution context được tạo ra theo một trình tự nhất định:
- B1: "Activation" object được tạo ra. Activation object lại là một thuật ngữ khác của trong ECMAScript specs. Nó có thể được xem như là một object vì cũng có các properties có thể truy xuất được. Nhưng nó không phải là object thông thường vì nó không có prototype (hoặc ít nhất thì prototype của nó không định nghĩa gì cả, undefined) và Activation object không được tham chiếu trực tiếp bằng Javascript code
- B2: Bước tiếp theo là tạo execution context cho function được gọi thực thi, ở bước này sẽ có sử xuất hiện của arguments object, là một object giống array với, có thể truy xuất các phần tử bên trong thông qua index (chỉ mục). Nó còn có cả length và callee properties. Ngoài ra một property cũng có tên là “arguments” thuộc sỡ hữu của Activation object, sẽ tham chiếu trực tiếp đến `arguments object.
- B3: Thực thể scope là một list (hay chain) chứa nhiều object, trong đó đứng đầu là Activation object. Ngoài ra bên trong function object sẽ có một property tên là [[scope]] (property này tham chiếu đến đâu thì ta sẽ được biết ở phần minh họa)
- B4: Sau đó quá trình khởi tạo biến (“variable instantiation”) xảy ra. Lúc này Activation object được ECMA-262 gọi với tên “Variable” object (lưu ý: “Activation” object và “Variable” object là một). Các parameter của function sẽ được gán vào các properties cùng tên, thuộc “Variable” object. Nếu parameter nào của function không được truyền giá trị đầu vào, thì perperty tương ứng với nó trong “Variable” object sẽ có giá trị là undefined. Ngoài ra, cả function object cũng được gán vào property cùng tên thuộc “Variable” object.
- B5: Các local variables nằm bên trong function được gán vào các properties cùng tên trong “Variable” object. Nhưng đặc biệt lưu ý rằng, các properties tương ứng với local variables, ban đầu tất cả đều mang giá trị undefined, chúng chỉ thật sử được gán giá trị khi function’s body bắt đầu thực thi.
- B6: Sau cùng là gán giá trị cho this keyword. Tùy theo cách gọi thực thi function mà this keyword sẽ được gán các giá trị khác nhau. (xem phần footnotes của bài viết sau)
Nếu ta nói global execution context tự động được tạo ra trong Creation Phase , quá trình đó diễn ra như thế nào, liệu có giống việc tạo một execution context của function như ta mô tả ở 6 bước bên trên không?
Global execution context tự động được tạo, không lệ thuộc vào việc gọi thực thi một function nào, do đó không có tham số đầu vào, arguments object lúc này không tồn tại, dẫn đến Activation object không cần thiết phải được tạo ra. scope trong tình huống này là global scope, chứa duy nhất một phần tử đó là global object. Quá trình hình thành Global execution context vẫn đi xuyên suốt thứ tự kể từ bước 4 ("variable instantiation"). Variable object cũng chính là global object. Ở bước 6, this keyword tham chiếu đến global object
bắt đầu cùng hình dung
giả sử ta có đoạn mã nguồn Javascript sau:
// 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…. } // .......
chú thích minh họa:
-khối chứ nhật màu xám là thực thể scope (xem lại định nghĩa thực thể scope tại B3)
-hình ellipse biểu diễn các object
-mũi tên màu đen với tên nằm bên trên là property
-mũi tên màu đỏ chú thích tại các thời điểm nhất định
Dựa vào các giải thích ở bên trên, khi khởi đầu với Creation Phase, browser đọc một lượt toàn bộ mã nguồn, hình thành global execution context:
- tiến hành hoisting các biến, tạo function (đối với những function được declaration), còn function c vì khai báo ở dạng function expression nên chỉ có biến c bị hoisting, còn bản thân function c tại thời điểm này vẫn chưa được tạo ra
- global object được tạo và ra vào cho vào global scope (theo B3). Variable object cũng chính là global object có các properties cùng tên và tham chiếu đến các đối tượng khác nằm trong danh sách global scope.
Tiếp theo đoạn code bên trên
// ....... MyFunction(); // call MyFunction
Khi MyFunction được gọi, execution context của nó đồng thời được tạo ra. Ta cùng duyệt lại 6 bước để hình thành nên MyFunction's execution context:
- Activation object được tạo ra
- arguments object được tạo ra
- tất cả được cho vào trong thực thể scope của MyFunction
- Variable object có các properties tham chiếu đến parameter của MyFunction (ở đây ko có parameter nào), và property trùng tên tham chiếu đến MyFynction's function object
- Variable object có các properties tham chiếu đến các local variable và inner function của MyFunction
- this keyword phụ thuộc vào cách mà MyFunction được gọi. Ở trường hợp này MyFunction được gọi thực thi theo phương pháp Function Format, Tức this keyword tham chiếu đến global object
Scope Chains và [[scope]] property của function object
(sẽ tìm hiểu tiếp về scope chain và closure ở p2)
referrences
(tham khảo từ bài viết Javascript Closure - FAQ notes)
(xem them Javascript Closure & scope chain with example)