Bài đăng nổi bật

Redo log, undo log và binary log

Đây là ba loại log mà bạn đã từng nghe khi tiếp cận mysql. Trong các cơ sở dữ liệu quan hệ (RDBMS) khác, cũng sẽ có các thành phần có vai tr...

Thứ Bảy, 13 tháng 2, 2016

Thám sát closure trong javascript

Trong bài viết này, tôi chỉ đề cập đến khái niệm closure trong javascript. Với các ngôn ngữ khác thì khái niệm này có thể khác nhau.

Trong javascript, chúng ta thống nhất một số điểm như sau:
  • Một variable khi không có bất cứ tham chiếu nào sẽ bị thu hồi.
  • Các local variables bên trong một function sẽ được re-create mỗi lần function đó được invoke.
  • Một function có thể được xem như một variable.
  • Javascript hỗ trợ lexical scoping.
Tại sao, tôi lại đề cập đến các điểm này? Vì dựa trên đó, khái niệm closure mới hình thành được trong javascript. Hai điểm đầu đã rõ ràng rồi, chúng ta lần lượt làm rõ hai điểm cuối.

Một function có thể được xem như một variable?

Hẳn các bạn đã quen với kiểu khai báo này.

var smile = function(){console.log("say cheese!");};

Một anonymous function được gán vào một variable. Bạn có thể cập nhật variable này bất cứ lúc nào bạn muốn

smile = function(){return "say cheese!";};

Nếu một function trả về một gía trị thì function đó có thể được dùng trong các expression như một variable thông thường. Về mặt bản chất, function cũng có thể trả về giá trị giống như một variable hay một expression.

Ở đây, chúng ta xem variable smile là một tham chiếu đến một anonymous function.

Javascript hỗ trợ lexical scoping?

Xem ví dụ sau:

var outer = function(){
       var outer_var = 10;
       var inner = function(){
             var inner_var=20;
             console.log("INNER LEVEL: outer_var = " + outer_var + " inner_var = " + inner_var);
       }
       inner();
       console.log("OUTER LEVEL: outer_var = " + outer_var + " inner_var = " + inner_var);
}

outer()
// Kết quả
INNER LEVEL: outer_var = 10 inner_var = 20
Uncaught ReferenceError: inner_var is not defined(…)

Inner level có thể truy cập đến environment của outer level nhưng từ outer level không thể truy cập đến environment của inner level. Đó chính là lexical scoping. Environment ở đây ám chỉ tập local variables của một function. Tập local variables của một function bao gồm các parameter và cả những variables được khai báo bằng var keyword nội trong function đó. Một cách dễ hiểu hơn, inner level kế thừa toàn bộ tập variables của outer level. Dù bạn có lồng bao nhiều function vào nhau thì lexical scoping vẫn đúng.

Closure là gì?

Chúng ta bắt đầu xem xét một ví dụ closure:

function multiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

var twice = multiplier(2);
console.log(twice(5));
// → 10

Điểm khác biệt ở đây là thay vì trả về một giá trị, function multiplier trả về một function. Vì trong javascript, một function có thể đối xử như một variable nên cách return này hoàn toàn hợp lệ. Anonymous function được trả lại có tham chiếu được đến một local variable factor của function multiplier. Anonymous function đóng vai trò là inner function còn multiplier đóng vai trò là outer function (còn gọi là enclosing function). Anonymous function truy cập được vào factor variable là hoàn toàn hợp lệ với lexical scoping. Do được anonymous function tham chiếu đến nên variable factor không bị hủy sau khi multiplier function thực hiện xong công việc. Anonymous function sau đó được gán lại cho twice variable nên bản thân anonymous function này cũng không bị thu hồi. Như tác giả Marijn Haverbeke trong cuốn eloquent javascript viết thì có thể xem anonymous function trả về như là một "freezing code" - một function chưa được active. Đoạn code cuối cùng, console.log(twice(5)); thực chất là active đoạn freezing code này. Kết hợp tham số truyền trực tiếp vào function là number cùng với tham chiếu đến factor variable nằm trong anonymous function nên cho ta kết quả là 10.

Tính chất tham chiếu đến trạng thái nhất định của local variables bên trong enclosing function gọi là closure và bản thân function mà giữ mối liên kết đến các local variables này của enclosing function chính là closure. Trong ví dụ trên anonymous function trả về trong multiplier chính là closure, multiplier chính là enclosing function còn factor chính là local variable được bao đóng vào closure.

Nếu thực hiện tiếp
var twice2 = multiplier(10);
console.log(twice2(20));
// → 200
console.log(twice(20));
// → 40

Do mỗi lần invoke function thì local variable trong function sẽ được tạo lại nên bạn có thể yên tâm là closure twice2 sẽ tham chiếu đến một factor variable mới có giá trị là 10 còn closure twice vẫn tham chiếu đến factor variable cũ có giá trị là 2. Bản thân twice và twice2 cũng là hai closure khác biệt.

Tận dụng tính chất này của closure mà các lập trình viên có thể giải quyết được "The infamous loop problem"

The infamous loop problem

Xét ví dụ sau:

function addLinks () {
     for (var i=0, link; i<5; i++) {
        link = document.createElement("a");
        link.innerHTML = "Link " + i;
        link.onclick = function () {
            alert(i);
        };
        document.body.appendChild(link);
    }
}

Bạn mong chờ click vào lần lượt từng link Link 0, Link 1, Link 2... sẽ alert ra tương ứng 0, 1, 2... Nhưng thực sự thì các sự kiện click đều chỉ alert ra 5. Tại sao vậy? Trong cách làm trên, function được gán vào sự kiện onclick nhưng không được thực hiện, bản thận giá trị i trong function thì luôn tham chiếu đến giá trị i đang hoạt động trong vòng lặp, hàm addLinks kết thúc thì giá trị i là 5 nên tất cả các sự kiện đăng ký khi đó sẽ tham chiếu đến giá trị 5 này.

Phải bằng cách nào đó tạo ra các bản sao của biến i này trong mỗi vòng lặp và gán vào onclick function. Thử tận dụng closure xem sao:

function addLinks () {
    for (var i=0, link; i<5; i++) {
        link = document.createElement("a");
        link.innerHTML = "Link " + i;
        link.onclick = function (num) {
            return function () {
                alert(num);
            };
        }(i);
        document.body.appendChild(link);
    }
}

Đoạn code trên hoạt động đúng như những gì chúng ta muốn. Click vào Link 0 thì alert 0, click vào Link 1 thì alert 1... Trong cách làm này, trạng thái biến i được truyền thẳng vào function qua một invoke nhưng sau đó trả về một closure, đây là một function đăng ký cho sự kiện onclick. Closure có khả năng tham chiếu đến các trạng thái của num parameter bên ngoài, bản thân biến num này cũng được tạo ra 5 lần khác trong từng lời gọi function.

Tham khảo

Cuốn eloquent javascript của Marijn Haverbeke
https://stackoverflow.com/questions/1451009/javascript-infamous-loop-issue
http://kipalog.com/posts/Closure-va-scope-trong-javascript
http://robertnyman.com/2008/10/09/explaining-javascript-scope-and-closures/

Không có nhận xét nào:

Đăng nhận xét