Function in Javascript Part 3: Closure – Is It Magic?

1. Functions as Value – First Look
Các biến hàm thường được sử dụng như một phần nhỏ của chương trình. Do vậy nhiều người sử dụng nó như một biến khai báo một lần và không bao giờ thay đổi. Điều này làm chúng ta dễ dàng nhầm lẫn giữa hàm và tên của nó.
Nhưng 2 điều này hoàn toàn khác nhau. Giá trị của hàm có thể làm mọi thứ một biến có thể làm – bạn có thể sử dụng nó trong các biểu thức, không chỉ gọi nó. Bạn có thể lưu giá trị của nó ở chỗ khác, pass một hàm như một argument vào hàm khác, vân vân….Tương tự như vậy, một biến hàm cũng chỉ như một biến thông thường và có thể gán vào biến khác, ví dụ thế này:

var chémgió = function() {
    hệthốngchémgió.khởiđộng("ngay lập tức");
};
if (cóphụhuynh){
    chémgió = function() {
    /* không làm gì cả */
    };
}

Trong các bài viết sau tôi sẽ nói về những điều tuyệt vời chúng ta có thể làm bằng cách truyền hàm này vào hàm kia. Bây giờ bạn chỉ cần thế này là đủ.

2. Closure – Is it magic?
Tính năng “coi hàm như biến” kết hợp với quy tắc “biến cục bộ được tạo lại mỗi lần gọi hàm” làm chúng ta đặt ra một câu hỏi: Điều gì sẽ xảy ra với một biến cục bộ nếu hàm được gọi đã “hết hạn sử dụng”?
Ví dụ như sau:

function wrapValue(n) {
    var localVariable = n;
    return function() { return localVariable; };
}
var wrap1 = wrapValue(1);
var wrap2 = wrapValue(2);
 
console.log(wrap1()); //in ra "1"
console.log(wrap2()); //in ra "2"

Tôi sẽ giải thích tuần tự chương trình trên như sau:

  • Đầu tiên chúng ta khai báo hàm ***wrapValue(n)***, hàm này tạo biến localVariable và trả về một hàm khác, hàm này lại trả về biến localVariable.

  • Ở dòng 5, tôi khai báo biến wrap1 = wrapValue(1) và dòng 8 in ra biến này (gọi đến hàm wrapValue(n) với n = 1).

  • Các bạn để ý tôi in ra ***wrap1()***, chứ không in ra wrap1, vì hàm wrapValue(n) trả về một hàm, bạn in ra wrap1 là nó sẽ in ra typeof wrap1 đấy nhé

  • Flow của chương trình lúc này đi vào hàm wrapValue(1)

  • Đầu tiên nó khai báo biến localVariable, có phạm vi toàn hàm

  • Ngay sau đó nó return lại cái gì đó. Là cái gì tôi chưa quan tâm. Tôi chỉ biết một điều: khi return xong là cái hàm wrapValue(1) đã “hết hạn sử dụng”, nghĩa là không còn tồn tại trong call stack nữa, đồng nghĩa với việc biến localVariable cũng không còn sử dụng được nữa.

  • Để xem nó return cái gì, à nó return lại một hàm khác. Tốt thôi, chúng ta sẽ đi vào xem hàm này trả về cái gì

  • Flow chương trình đi vào hàm function() không tên kia, và á đù, nó trả về biến localVariable, mà đáng ra, không còn tồn tại nữa

  • Ơ nhưng cuối cùng nó vẫn in ra chính xác giá trị của biến localVariable kìa? Tương tự với hàm ***wrapValue(2)***. In ra giá trị của một biến đã không còn tồn tại nữa?! Is it magic?

3. A closer look into Closure – It isn’t magic!
À vâng, khái niệm closure khiến tôi cũng hơi sợ. Tôi đang theo học khóa node.js trên Techmaster.vn, đến đoạn này thì tôi thấy khó hiểu và thầy Trịnh Minh Cường không nói gì thêm chỉ yêu cầu Google, thế là tôi đã quyết định dừng học tiếp node.js và “lên đường” đi bắt con ma closure.
Trước hết, ta phải xem Closure là cái gì đã. Sau một hồi truy đuổi, tôi đã tổng kết được về con ma Closure này như sau:

  • Tổng kết kiểu bình thường (không đúng lắm nhưng dễ hình dung): Một Closure là một biến hàm (biến hàm # biến trong hàm) mà vẫn tồn tại sau khi hàm đã được return.

  • Tổng kết kiểu nguy hiểm: Một Closure là một stack-frame không bị hủy khi hàm return.

Stop here!
Nếu bạn vẫn chưa hiểu gì, chưa có khái niệm gì, chưa tưởng tượng ra gì, vui lòng học lại Javascript: Function. Nếu bạn hiểu lơ mơ rồi, hãy tiếp tục đi tìm hiểu “con ma” Closure với tôi.

Ở đây chúng ta cần nói đến một bức tường cực kì kiên cố trong suy nghĩ của những ai đã từng học qua C, C++ và đang không hiểu closure là gì: Bạn có thể đang phân tích rằng: Khi ta return cái function vô danh kia là ta đang return một con trỏ trỏ đến cái hàm vô danh đấy. Và cái wrap1 = wrapvalue(1) kia đang trỏ đến cái hàm wrapValue(1). Vâng cả 2 điều này là đúng nhưng chưa đủ. Trong C, C++, Java và rất nhiều ngôn ngữ khác, stack-frame chứa hàm sẽ bị hủy sau khi hàm trả về. Nhưng trong javascript, bất cứ khi nào bạn tạo ra một hàm trong một hàm khác, thì nghĩa là bạn cũng đang tạo ra một Closure. Và bạn có thể hiểu là cái function mà tôi in ra trong ví dụ trên – wrap1(), nó vừa trỏ đến hàm trả về, vừa có một con trỏ ẩn khác trỏ đến cái hàm đáng ra đã bị hủy – wrapValue(1). Cái hàm này không bị hủy, vâng nó vẫn còn tồn tại ở đấy. Vậy cuối cùng Closure là cái nào? Chiếu theo định nghĩa nguy hiểm ở kia, khi tôi khai báo wrap1 = wrapValue(1) –> Đây là một Closure. Nó trỏ đến hàm wrapValue(1), và hàm này không hề bị hủy sau khi return.

Thêm một ví dụ nữa cho dễ hình dung:

function incrementValue(n){
    var number = n;                 //Thằng này đã, đang và sẽ sống trong một closure
    var printNumber = function(){   //Thằng này vẫn trỏ đến hàm incrementValue và lấy giá trị của number
        console.log(number);
    };
    number++;
    return printNumber;
}
var test = incrementValue(0); //Thằng này là một closure
test(); //in ra 1
  1. Trong ví dụ trên tôi khai báo hàm incementValue(n), theo thứ tự sẽ tăng nó lên 1 đơn vị, sau đó trả về hàm printNumber. Nhưng mà tôi khai báo hàm printNumber lúc chưa tăng nó lên 1 đơn vị.

  2. Tôi gọi test(). Hàm test = incrementValue(0) là một closure.

  3. Stack-frame của hàm incrementValue(0) được tạo

  4. Number được tăng lên từ 0 thành 1

  5. Hàm printNumber được trả về, nhưng Stack-frame của hàm incrementValue vẫn chưa bị hủy.
    Lúc này test = incrementValue(0) tương đương test = printNumber, và đồng thời printNumber trỏ đến incrementValue(n), do đó test trỏ đến đồng thời hai hàm: printNumber()incrementValue(0). test lấy giá trị của number mà function() nó đang trỏ đến yêu cầu.

  6. In ra

3 Likes

Hay qúa, ngồi lay hoay mãi không hiểu closure là gì, đã thông não. love :heart_eyes:

83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?