Tìm hiểu javascript closures

Như đã nói trong bài tìm hiểu function JavaScipt, bài viết này tôi sẽ giới thiệu với các bạn về JavaScript closures. Việc hiểu closures sẽ giúp bạn viết code tốt hơn (ngắn gọn, súc tích hơn và thậm chí dễ hiểu hơn,…). Tuy nhiên, trước khi đi vào những ưu, nhược điểm của việc sử dụng closures, chúng ta sẽ xem closures là gì và cách sử dụng nó như thế nào.

Khái niệm JavaScript closures

Closures là tập hợp bao gồm một hàm và môi trường nơi hàm số đó được khai báo. Ở đây, môi trường bao gồm tất cả những biến cục bộ trong phạm vi hàm số được khai báo (để đơn giản, từ nay tôi sẽ sử dụng hàm closures khi nói về JavaScript closures).

Hàm closures có thể truy cập biến số ở 3 phạm vi khác nhau là:

  • Biến toàn cục (global)
  • Biến được khai báo ở hàm số chứa hàm closures (outer function)
  • Biến ở trên trong hàm closures

Ví dụ:

var YEAR = '2017';
function greet(name) {
 var intro = 'Hello ';
 function sayHello(){
   var hello = intro + name + ' in ' + YEAR;
   console.log(hello);
 }
 sayHello();
}
greet('Lam Pham');
// => Hello Lam Pham in 2017

Ở đây, hàm sayHello là một hàm closures. Hàm này có thể sử dụng biến số ở toàn cục là YEAR, biến ở hàm chứa nó là introhello.

Một số đặc điểm quan trọng của JavaScript Closures

Nếu bạn muốn hiểu sâu và vận dụng closures thì sau đây là những đặc điểm quan trọng mà bạn cần nắm vững.

Hàm closures có thể truy cập tới biến số của hàm chứa nó, dù cho hàm đó đã return

Thông thường, khi một hàm số đã return thì biến cục bộ trong nó cũng sẽ được giải phóng. Nhưng với closures, bạn vẫn có thể truy cập đến những biến cục bộ đó ngay cả khi outer function đã thực hiện xong.

function adder(n){
 var intro = 'This answer is ';
 var local = n;
 return function(number){
   var result = number + local;
   console.log(intro + result);
 }
}
var adder2 = adder(2);
adder2(10);
// => This answer is 12

Trong ví dụ trên, hàm closures là một hàm số không tên function(number). Hàm closures này sử dụng biến cục bộ của outer functionintrolocal.

Khi tôi gọi hàm adder(2), hàm số này thực hiện và kết quả trả về được gán vào biến adder2. Khi gọi adder2(10), kết quả trả về là 12, chứng tỏ hàm closures vẫn có thể truy cập tới biến cục bộ của outer function khi hàm đó thực hiện xong.

Hàm closures lưu trữ biến số của outer function theo kiểu tham chiếu

Xét ví dụ dưới đây:

function ObjId(){
    var id = 1;
    return {
        getId: function(){
           return id;
        },
        setId: function(_id){
           id = _id;
        }
    }
}
 
var myObject = ObjId();
console.log(myObject.getId());  // => 1
myObject.setId(10);
console.log(myObject.getId());  // => 10

Hàm objId trả về một đối tượng bao gồm 2 hàm closures là getIdsetId. Các hàm closures này sử dụng chung một biến cục bộ là id.

Ban đầu, tôi gọi myObject.getId() thì kết quả trả về là 1 (giá trị của biến cục bộ). Sau đó, tôi gọi myObject.setId(10). Nếu hàm closures chỉ lưu biến cục bộ theo giá trị thì nghĩa là giá trị của biến cục bộ id sẽ không thay đổi. Nhưng khi tôi gọi tiếp myObject.getId() thì giá trị trả về là 10. Chứng tỏ, hàm closures phải lưu biến cục bộ theo kiểu tham chiếu.

Bây giờ đọc lại ví dụ trên một lần nữa, bạn có thấy nó giống với cách sử dụng class (lớp) trong C++ hay Java không? Tôi thì thấy có giống. Thực ra đây là một trong những cách tạo đối tượng (object) trong JavaScript mà tôi sẽ giới thiệu trong những bài viết tiếp theo.

So sánh hiệu năng giữa việc sử dụng closures và prototype

Ví dụ trên minh hoạ cách sử dụng closures. Bây giờ tôi sẽ viết lại nó theo cách sử dụng prototype:

function ObjId(){
    this.id = 1;
}
 
ObjId.prototype.getId = function(){
    return this.id;
};
 
ObjId.prototype.setId = function(_id){
    this.id = _id;
};
 
var myObject = new ObjId();
console.log(myObject.getId());  // => 1
myObject.setId(10);
console.log(myObject.getId());  // => 10

Kết quả trả về vẫn giống y hệt như trên. Tuy nhiên, cách nào sẽ chạy nhanh hơn? Closures hay Prototype?

Dựa theo kết quả so sánh tại bài viết Performance of prototype vs closure code in JavaScript, tôi tóm tắt lại như sau:

  • Định nghĩa đối tượng: JavaScript closures nhanh hơn.
  • Khả năng tiết kiệm bộ nhớ và khởi tạo đối tượng mới: Prototype nhanh hơn.
  • Truy cập hàm (getter, setter): Prototype nhanh hơn.

Một cách khách quan, việc sử dụng prototype sẽ nhanh hơn và tiết kiệm bộ nhớ hơn việc sử dụng closures. Tuy nhiên, tôi vẫn không phủ nhận vai trò của closures trong việc giúp bạn viết code ngắn gọn và rõ ràng hơn. Và dù sao, việc sử dụng closures là không bắt buộc. Tất cả tuỳ thuộc vào bạn.

Bài viết trên là khá ngắn gọn. Hy vọng nó phần nào giúp bạn hiểu được JavaScript closures là gì và cách sử dụng closures.

Tham khảo


Theo dõi Lam Pham trên Daynhauhoc để nhận thông báo khi có bài viết mới nhất:

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