CLOSURE TRONG JAVACRIPT

1. Closure là gì?

Trong các ngôn ngữ lập trình máy tính, một closure là một function hay một tham chiếu đến một function, cùng với một môi trường tham chiếu. Môi trường tham chiếu ở đây là một bảng lưu trữ các tham chiếu đến các biến không cục bộ của function đó(non-local variables). Không giống như con trỏ hàm, một closure cho phép một function có thể truy nhập đến các biến không cục bộ ngay cả khi các biến này nằm ngoài phạm vi (scope) của nó. Closure làm được điều này chính bởi vì nó còn giữ một tham chiếu đến một môi trường tham chiếu mà ở đó các biến không cục bộ này được lưu trữ.

Closure được sử dụng trong rất nhiều các ngôn ngữ lập trình, đặc biệt đối với các ngôn ngữ thông dịch như Python, Ruby, Javascript …. Về cơ bản thì chúng có cơ chế hoạt động giống nhau, bài viết dưới đây sẽ đi chi tiết về việc sử dụng closure trong javascript.

Để dễ hình dung trước tiên hãy sẽ xem xét đoạn mã sau:

Capture11.png

Khi chạy test sẽ ra kết quả:

js2.png

Thật ngạc nhiên là biến innerValue vẫn còn tồn tại và được in ra, bởi lẽ ra thông thường sau khi hàm functionOuter() chạy xong thì biến innerValue phải được giải phóng khỏi bộ nhớ và nó không thể được in ra nữa. Điều này xảy ra chính là nhờ cơ chế lưu tham chiếu môi trường của closure.

js3.png

Giống như một quả bong bóng bảo vệ, closure của hàm innerFunction() giúp cho các biến trong phạm vi (scope) của nó không bị bộ thu lượm rác thu hồi. Khi khai báo innerFunction() bên trong outerFunction(), không chỉ hàm được định nghĩa mà một closure cũng được tạo ra, nó giữ tham chiếu đến tất cả các biến trong phạm vi tại điểm khai báo hàm. Ở ví dụ trên closure của hàm innerFunction() được tao ra và giữ tham chiếu đến các biến later, innerValue, outerValue và hàm outerFunction(). Do đó cho dù hàm outerFunction() thực thi xong thì các biến này cũng không bị bộ lượm rác thu gom vì closure của innerFunction() vẫn còn giữ tham chiếu đến chúng. Đó là lý do vì sao mà các biến này vẫn có thể được sử dụng và in ra như trong đoạn mã đã đề cập ở trên.

2. Khi nào sử dụng closure?

  • Được sử dụng như getter và setter

Closure được dùng phổ biến để đóng gói một số thông tin như là các biến private. Việc sử dụng các đoạn mã hướng đối tượng trong javascript là không thể để sử dụng các biến private giống , các ngôn ngữ khác như Java hay C#, tuy nhiên bằng cách sử dụng nguyên lý của closure chúng ta hoàn toàn có thể làm được điều tương tự, đoạn code dưới đây sẽ môt tả chi tiết

encapsulation.png

Chúng ta có thể thấy trên hình vẽ, biến feints là một biến local của function (được sử dụng như một class Ninja), nó chỉ có phạm vi trong sử dụng bên trong function và không thể truy nhập từ bên ngoài, function this.getFeints() trỏ đến một closure mà closure này lại có tham chiếu môi trường đến biến feints, do đó nó hoàn toàn có khả năng truy nhập được vào biến này. Cũng tương tự như vậy nếu chúng ta hoàn toàn có thể tạo ra một hàm setFeints() để thay đổi giá trị của faints. Như vậy 2 closure này đóng vai trò như các setter và getter trong các ngôn ngữ lập trình hướng đối tượng bậc cao.

  • Sử dụng như một decorator

Giả sử công ty Apple bán các gói sản phẩm dành cho máy Macbook bao gồm 3 gói, mỗi gói sẽ bao gồm giá cơ bản cộng thêm chi phí của gói đó đi kèm:

  • Gói 1 ram 64G chi phí máy sẽ phải cộng thêm 75$.

  • Gói 2: Có trạm khắc chi phí cộng thêm 200$.

  • Gói 3: Có bảo hiểm chi phí cộng thêm 250$.

Đoạn mã dưới đây sẽ thực thi các decorator cho hàm chi phí cost() của máy Macbook

//What we're going to decorate
function MacBook() {
   this.cost = function () { return 997; };
   this.screenSize = function () { return 13.3; };
}
/*Decorator 1*/
function Memory(macbook) {
  var v = macbook.cost();
  macbook.cost = function() {
    return v + 75;
  }
}
/*Decorator 2*/
function Engraving( macbook ){
  var v = macbook.cost();
  macbook.cost = function(){
    return  v + 200;
  };
}
/*Decorator 3*/
function Insurance( macbook ){
  var v = macbook.cost();
  macbook.cost = function(){
    return  v + 250;
  };
}

var mb = new MacBook();

Memory(mb);

Engraving(mb);

Insurance(mb);

console.log(mb.cost()); //1522

Các decorator đã ghi đè method superclass.cost() để trả lại giá trị hiện tại của Macbook công với giá trị các gói đi kèm đã được chỉ định. Mỗi khi người dùng muốn dùng gói nào, chúng ta chỉ cần gọi decorator tương ứng của nó và truyền giá trị hiện tại của máy Macbook thì giá trị sau cùng sẽ được tính

  • **Binding function contexts **

Trong Javascript các phương thức call() và apply() có thể được sử dụng để giải quyết vấn đề về context của một function,việc sử dụng chúng cho phép chúng ta truyền context bất kỳ cho một function,tuy nhiên đôi khi việc chưa thực sự hiểu rõ về nó cũng mang lại cho chúng ta những rắc rối.Xem xét đoạn mã dưới đây, khi một function đóng vai trò như một listener để bắt sự kiện click của một button.

<button id= "test">Click Me!</button>
<script type=”text/javascript”>
var button = {
  clicked: false;
  click: function() {
  this.clicked = true;
  if (button.clicked) {
    alert("The button has been clicked")
  }
}

var elem = document.getElementById("test");
elem.addEventListener("click", button.click, false);
</script>

Trong ví dụ trên, chúng ta có một button và muốn biết xem liệu button này đã được click hay chưa. Để lưu giữđược trạng thái này, chúng ta tạo một object button khác và định nghĩa một method đóng vai trò như một bộ xử lý sự kiện(event handler) khi button được click. Tuy nhiên khi chạy test và click vào button thì kết quả không như mong đợi khi không có một dialog nào được hiển thị. Đoạn code trên lỗi là do context của function.click() tham chiếu đến không phải là object button như chúng ta mong muốn mà là button(elem), bởi bộ xử lý sự kiện(event-handling system) cuả trình duyệt định nghĩa context của lời gọi hàm chính là element của sự kiện nên context của function click() chính là button (Click Me!), vì vậy mà chúng ta đã thiết lập nhầm trạng thái cho element button thay vì object button

May mắn là chúng ta có thể sửa lại đoạn mã trên bằng cách sử dụng kết hợp anonymous function, apply() và closure

<script type="text/javascript">
function bind(context,name){
  return function(){
    return context[name].apply(context,arguments);
  };
}
var button = {
  clicked: false,
  click: function(){
    this.clicked = true;
    assert(button.clicked,"The button has been clicked");
    console.log(this);
  }
};
var elem = document.getElementById("test");
elem.addEventListener("click",bind(button,"click"),false);
</script>

Sự khác biệt lớn nhất ở đoạn mã này với đoạn mã trước đó là chúng ta đã add thêm method bind(). Method nàytạo mới và trả về một anonymous function được dùng để gọi function chính bằng cách sử dụng phương thức appy()vì vậy mà chúng ta có thể truyền bất kỳ context nào cho function chính mà chúng ta muốn cùng với các tham số của nó.Tiếp đó chúng ta tạo một bộ xử lý sự kiện thông qua sử dụng phương thức bind() thay vì sử dụng trực tiếp phương thức button.click. Do đó function được được wrap trong phương thức bind() trở thành bộ xử lý sự kiện. Đây là lý do vì sao khi nút button được click, anonymous function này sẽ được gọi, và hàm này sẽ gọi tiếp đến method click cùng với context được truyền vào là object button. Hình ảnh dưới đây minh họa cho quá trình nói trên:

Ngoc.png

Tài liệu tham khảo

Secrets of the Javascript Ninja Chapter5: closing in on closures


All Rights Reserved