Bản dịch này chưa hoàn thành. Xin hãy giúp dịch bài viết này từ tiếng Anh.

Closure là một hàm được viết lồng vào bên trong một hàm khác (hàm cha) nó có thể sử dụng biến toàn cục, biến cục bộ của hàm cha và biến cục bộ của chính nó (lexical scoping)

Lexical scoping

Xem xét ví dụ sau

function init() {
  var name = 'Mozilla'; // name là biến cục bộ của hàm init
  function displayName() { // displayName() là hàm closure
    alert(name); // sử dụng biến của hàm cha
  }
  displayName();
}
init();

init() tạo một biến cục bộ name và một hàm displayName(). Hàm displayName() được khai báo bên trong hàm init() và chỉ tồn tại bên trong hàm  init() . Hàm displayName() không có biến cục bộ nào của chính nó. Tuy nhiên, hàm displayName() truy cập đến biến name vốn được định nghĩa ở hàm cha, init(). Nếu bên trong hàm displayName() có khai báo biến cục bộ của chính nó, biến đó sẽ được sử dụng.

Thực thi đoạn code trên sẽ nhận được kết quả từ alert() bên trong hàm displayName() , giá trị biến name . Đây là một ví dụ của lexical scoping,  cách các biến được truy cập như thế nào khi hàm được lồng nhau. Hàm lồng bên trong có thể truy suất đến biến được khai bào từ hàm bên ngoài.

Closure

Giờ xem xét đến ví dụ sau:

function makeFunc() {
  var name = 'Mozilla';
  function displayName() {
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();

Chạy đoạn code trên sẽ nhận kết quả tương tự như ví dụ hàm init() ở trên; sự khác nhau ở đây là gì? khi gọi hàm makeFunc() sẽ return về hàm displayName() ,  và chưa hề chạy qua đoạn code trong hàm displayName().

Thoạt nhìn, đoạn code này sẽ không dễ nhận ra đoạn code này vẫn chạy bình thường. Trong một số ngôn ngữ lập trình khác, biến cục bộ bên trong một hàm chỉ tồn tại trong quá trình hàm thực thi. Một khi makeFunc() chạy xong, chúng ta sẽ nghĩ rằng biến name sẽ không còn thể truy cập được. Tuy nhiên, đoạn code trên sẽ vẫn cho ra kết quả không khác gì ví dụ ở trên cùng, rõ ràng đây là một tính chất đặc biệt của Javascript.

Trong trường hợp này, myFunc đang tham chiếu đến một instance displayName được tạo ra khi chạy makeFunc. Instance của displayName sẽ duy trì lexical environment, biến name sẽ vẫn tồn tại. Với lý do này, khi gọi hàm myFunc , giá trị biến name vẫn có và chuỗi "Mozilla" sẽ được đưa vào hàm alert.

Một ví dụ thú vị khác — hàm makeAdder:

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

Trong ví dụ này, chúng ta định nghĩa hàm makeAdder(x), nhận vào 1 argument, x, và trả về một hàm khác. Hàm trả về nhận vào 1 argument, y, và trả về kết của của x + y.

Bản chất, makeAdder là một hàm factory — nó tạo ra một hàm khác nhận một argument. Ví dụ trên chúng ta sử dụng hàm factory để tạo ra 2 functions — cái thứ nhất thêm argument là 5, cái thứ 2 thêm 10.

add5 và add10 đều  là closures. Cùng một xử lý bên trong, nhưng được lưu ở  lexical environments khác nhau. Trong lexical environment của add5 , x = 5, trong khi lexical environment của add10, x = 10.

Ứng dụng closures

Closures hữu dụng vì nó cho phép chúng ta gắn một vài dữ liệu (bên trong lexical environment) với một function sẽ tương tác với dữ liệu. Tương tự như trong object-oriented programming, các object cho phép chúng ta gắn một vài dữ liệu với một hoặc nhiều phương thức bên trong

Trên nền web, hầu hết code được viết bằng JavaScript là event-based — chúng ta định nghĩa một xử lý, sau đó gắn nó vào event sẽ được gọi bởi user (ví dụ như click hay keypress). Đoạn code của chúng ta sẽ là callback: 1 function chạy khi có một sự kiện xảy ra.

Ví dụ, giả sử chúng ta muốn thêm một cái button để thay đổi kích thước chữ. Một trong những cách làm là set font-size cho thẻ body bằng giá trị pixels, sau đó set kích thước của những phần từ khác (như header) sử dụng đơn vị em :

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}

h1 {
  font-size: 1.5em;
}

h2 {
  font-size: 1.2em;
}

Khi thay đổi font-size của thẻ body , kích thước font của h1, h2 sẽ tự động được điều chỉnh.

Trong JavaScript:

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

size12, size14, và size16 là những hàm sẽ thay đổi kích thước font chữ của body qua 12, 14, và 16 pixels. Gắn cho các button tương ứng:

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a> 

Giả lập phương thức private với closures

Những ngôn ngữ như Java chúng ta có cách để khai báo các phương thức private, nghĩa là phương thức chỉ được gọi bởi các phương thức khác nằm cùng class.

JavaScript không hỗ trợ cách làm chính quy cho việc này, tuy nhiên có thể giả lập việc này bằng closures. Phương thức Private không chỉ hữu dụng trong việc giới hạn việc truy cập: nó còn là một cách rất tốt để quản lý global namespace, giữ các phương thức không cần thiết có thể làm xáo trộn những phương thức public.

Đoạn code bên dưới diễn giải cách sử dụng closures để khai báo một phương thức public có thể truy cập phương thức private và biến. Sử dụng closures như thế này gọi là module pattern:

var counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };   
})();

console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1

Mỗi closure có một lexical environment. Ở đây, chúng ta tạo 1 lexical environment cho cả 3 function: counter.increment, counter.decrement, and counter.value.

Lexical environment được tạo bên trong một hàm không tên function, sẽ được tạo ra ngay khi được gán cho một khai báo. Lexical environment chứa 2 private: biến  privateCounter và hàm changeBy. Cả 2 đối tượng private đều không thể được truy cập trực tiếp từ bên ngoài. Thay vào đó, nó chỉ có thể tương tác thông qua 3 phương thức public.

Cả 3 phương thức public đều là closures chia sẽ cùng 1 Lexical environment. Cả 3 đều có thể truy cập đến privateCounter và changeBy

Chúng ta khai báo một  hàm không tên tạo counter, và gọi nó ngay lập tức rồi gắn vào biến counter . Chúng ta lưu hàm này vào một biến khác makeCounter và sử dụng nó để tạo ra nhiều counter khác

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }  
};

var counter1 = makeCounter();
var counter2 = makeCounter();
alert(counter1.value()); /* Alerts 0 */
counter1.increment();
counter1.increment();
alert(counter1.value()); /* Alerts 2 */
counter1.decrement();
alert(counter1.value()); /* Alerts 1 */
alert(counter2.value()); /* Alerts 0 */

Để ý cách 2 counters, counter1 và counter2, hoàn toàn độc lập với nhau. Mỗi closure tham chiếu đến các instance khác nhau của privateCounter .

Sử dụng closures bằng cách này cho ta rất nhiều ưu điểm như trong object-oriented programming -- cụ thể, dữ liệu được ẩn đi và đóng gói.

Closure Scope Chain

Mỗi closure chúng ta có 3 scopes:-

  • Scope cục bộ
  • Scope của function chứa closure
  • Scope global

Chúng ta có thể truy cập đến cả 3 scope này trong closure tuy nhiên sẽ ra sau nếu chúng lồng nhiều closure với nhau. Như ví dụ sau:

// global scope
var e = 10;
function sum(a){
  return function(b){
    return function(c){
      // outer functions scope
      return function(d){
        // local scope
        return a + b + c + d + e;
      }
    }
  }
}

console.log(sum(1)(2)(3)(4)); // log 20

// chúng ta có thể không dùng hàm không tên:

// global scope
var e = 10;
function sum(a){
  return function sum2(b){
    return function sum3(c){
      // outer functions scope
      return function sum4(d){
        // local scope
        return a + b + c + d + e;
      }
    }
  }
}

var s = sum(1);
var s1 = s(2);
var s2 = s1(3);
var s3 = s2(4);
console.log(s3) //log 20

Với ví dụ trên, chúng ta có thể nói toàn bộ closure sẽ có cùng scope với function cha.

Tạo closures trong vòng lặp: lỗi thường thấy

Trước khi có từ khóa let keyword được giới thiệu trong ECMAScript 2015, một lỗi thường gặp trong closure khi nó được tạo bên trong vòng lặp. Xem ví dụ sau:

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp(); 

Mảng helpText khai báo 3 string help, tương ứng cho mỗi ID của input. Vòng lặp chạy qua cả 3 khai báo này, chèn vào sự kiện onfocus để hiển thị đoạn string phù hợp với từng input.

Nếu thử chạy đoạn code này, bạn sẽ thấy kết quả không giống như chúng ta nghĩ. Mặc cho chúng ta đang focus vào input nào, dòng message hiển thị sẽ luôn là "Your age (you must be over 16)".

Lý do là hàm gắn cho sự kiện onfocus là closures; nó sẽ thống nhất các khai báo trong và đưa vào chung scope của hàm setupHelp. Cả 3 closures được tạo trong vòng lặp, nhưng cùng chung lexical environment, tức là dùng chung biến item.help. Giá trị item.help được xác định khi onfocus được gọi. Vì ở đây vòng lặp đã chạy đến giá trị cuối cùng của mảng, biến item sẽ trỏ đến giá trị cuối cùng trong mảng.

Giải pháp trong tình huống này là dùng thêm một closures: như cách chúng ta viết function factory trước đó:

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp(); 

Hàm makeHelpCallback đã tạo ra một lexical environment riêng cho mỗi callback.

Một cách khác là sử dụng closure không tên

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    (function() {
       var item = helpText[i];
       document.getElementById(item.id).onfocus = function() {
         showHelp(item.help);
       }
    })(); // Immediate event listener attachment with the current value of item (preserved until iteration).
  }
}

setupHelp();

Nếu không muốn sử dụng nhiều closure, có thể dùng từ khóa let được giới thiệu trong ES2015 :

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    let item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

Ví dụ này ta sử dụng let thay cho var, như thế mỗi closure được gán cho 1 biến block-scoped.

Một cách khác nữa là dùng forEach() để lặp qua mảng helpText và gắn hàm xử lý <div>, như bên dưới:

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];
  
  helpText.forEach(function(text) {
    document.getElementById(text.id).onfocus = function() {
      showHelp(text.help);
    }
  });
}

setupHelp();

Cân nhắc về hiệu năng

Dùng closure trong những trường hợp thực sự không cần thiết thì không khôn ngoan vì nó có thể ảnh hưởng hiệu năng lúc chạy.

Một ví dụ, khi tạo mới một object/class, phương thức thường nên gán vào object mà không nên khai báo bên trong hàm khởi tạo của object. Lý do là mỗi khi hàm constructor được gọi, phương thức sẽ được gán lại một lần nữa trên mỗi một object được tạo ra

Ví dụ cho trường hợp sau:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

Bởi vì đoạn code trên không thực sự cần những lợi ích có được từ closure trên mỗi instance, chúng ta có thể viết lại mà không sử dụng closure:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};

Tuy nhiên, khai báo lại prototype không được khuyến khích. Chúng ta mở rộng prototype bằng cách sau:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

Trong 2 ví dụ trên, tất cả object sẽ kế thừa cùng những prototype và khai báo phương thức trên mỗi object không bắt buộc. Xem Details of the Object Model để tìm hiểu thêm

Document Tags and Contributors

Những người đóng góp cho trang này: quytran, luubinhan
Cập nhật lần cuối bởi: quytran,