Closure trong Javascript
Closures
là một hàm được viết lồng trong một hàm khác (hàm cha), nó có thể sử dụng biến toàn cục(global variable) hoặc biến cục bộ của hàm cha (outer variable) hoặc biến cục bộ trong nó (local variable).
Lexical scoping
Lexical scope là một quy ước được sử dụng trong nhiều ngôn ngữ lập trình để thiết lập phạm vi của các biến chứa trong một hàm (scope) để nó chỉ có thể được gọi trong khối lệnh đã được hàm đó định nghĩa. Phạm vi này được xác định khi code được thực thi.
Xét VD 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();
Ở VD trên, init
là một hàm được gọi, trong init
khai bá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 chưa bất kỳ biến cục bộ nào. Tuy nhiên, nó lại có thể tiếp cận với biến name
được định nghĩa từ hàm cha, init()
. Nếu bên trong hàm displayName()
có khai báo biến cục bộ cho chính nó, biến đó sẽ được sử dụng.
function init() { var name = 'Hello World'; // name là biến cục bộ của hàm init function displayName() { // displayName() là hàm closure console.log(name); // sử dụng biến của hàm cha } displayName(); } init();
Thực thi đoạn code trên sẽ nhận được kết quả từ console.log()
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
Trong các ngôn ngữ lập trình, một closure cũng như lexical scope hay hàm closure là một kỹ thuật để thực thi một hàm lồng trong một hàm bậc cao hơn, đặc biệt trong Javascript, khi nhắc đến hàm nó được xem như "một công dân hạng 1". Về nguyên lý hoạt động, closure là một bản ghi lưu trữ một hàm cùng với một môi trường --Wikipedia--
Xét VD sau :
function makeFunc() { var name = 'Hello World'; function displayName() { console.log(name); } return displayName; } var myFunc = makeFunc();
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 "Hello World" sẽ được đưa vào hàm console.log()
.
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 lập trình hướng đối tượng (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.
Trong lập trình web, hầu hết code được viết bằng JavaScript là hướng sự kiến (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 JS:
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
, và 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
Giả sử 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 gặp
Trước khi có từ khóa let
đượ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();
Hiệu suất
Dùng closure trong những trường hợp thực sự không cần thiết có thể ảnh hưởng đến hiệu suất hoạt động khi chương trình thực thi.
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
Bài viết đến đây là hết, cám ơn sự theo dõi của các bạn.