Có lẽ bạn chưa biết rằng, có rất nhiều người “rớt phỏng vấn xin việc” chỉ vì Closure đấy! Vậy, Closure là gì? Tại sao Closure lại xuất hiện nhiều trong những buổi phỏng vấn đến như vậy? Đừng lo, Tino Group sẽ giải đáp những câu hỏi này và giúp bạn hiểu thêm về Closure trong JavaScript thông qua một số ví dụ nhé!
Tìm hiểu về Closure
Closure là gì?
Closure là một hàm được viết lồng vào trong một hàm khác và Closure có thể sử dụng biến toàn cục, biến cục bộ của hàm (hàm cha) ngay cả khi đã đóng và sử dụng biến cục bộ của bản thân Closure.
Trong JavaScript, Closure là một thuộc tính rất mạnh. Không chỉ JavaScript, Closure còn có mặt trong hầu hết các loại ngôn ngữ lập trình khác.
Trong JavaScript, Closure có 3 phạm vi truy cập biến số khác nhau bao gồm:
- Biến ở trong hàm Closure
- Biến được khai báo ở hàm cha chứa Closure (outer function)
- Biến toàn cục (global)
Hàm Closure ra sao?
Bên cạnh lý thuyết, chúng ta sẽ đi tìm hiểu về ví dụ để rõ hơn về Closure nhé!
Trước tiên, ta sẽ xem xét ví dụ về Lexical scoping như sau:
function outerFuc() {
var name = 'Mai Truc Lam';
function innerFunc() {
console.log(name);
}
innerFunc();
}
outerFuc(); // Kết quả: Mai Truc Lam
Trong đó, ta có:
- Biến toàn cục: name
- Function functionOuter(function cha)
- Function innerFunc, trong function innerFunc không có biến toàn cục nào nhưng trong hàm đang sử dụng một biến name của function functionOuter.
Thay đổi code một tí, chúng ta sẽ có Closure như sau:
function outerFuc() {
var name = 'Mai Truc Lam';
function innerFunc() {
console.log(name);
}
return innerFunc;
}
var refInnerFunc = outerFuc();
refInnerFunc(); // Kết quả: Mai Truc Lam
Có vẻ, bạn cũng đã thấy sự khác biệt ở đây rồi đúng không nào? Trong đoạn code thứ 2, chúng ta sẽ thấy:
Hàm refInnerFunc đang gáng và tham chiếu đến kết quả của hàm outerFuc(); refInnerFunc trỏ đến hàm innerFunc nhưng chưa thực thi.
Sau khi hàm outerFuc() thực thi xong, biến toàn cục của hàm sẽ được giải phóng
Khi hàm innerFunc() thực thi sẽ ra kết quả là Mai Truc Lam. Nhưng kết quà này là của hàm cha outerFuc(). Nhưng điều này nghe có vẻ vô lý về mắt lý thuyết đúng không?
Nhưng trong JavaScript, khi 1 hàm nằm trong 1 hàm khác, nếu hàm cha thực thi trước sẽ tạo ra một môi trường Lexical đặt tất cả các biến đang có vào và gắn cho hàm con (trong ví dụ là: refInnerFunc) để sử dụng.
Cũng có thể giải thích như sau: khi hàm outerFuc() “chết” đi sẽ để lại “di chúc” cho hàm con refInnerFunc() là một biến name, sau khi hàm innerFunc() “chết” theo biến mới được giải phóng.
Trong ví dụ trên, chúng ta có thể thấy được hàm refInnerFunc() là Closure, vì hàm này đang ở trong 1 hàm khác và có thể sử dụng biến name của hàm cha outerFuc(). Nếu có cả biến cục bộ của bản thân refInnerFunc() và biến global, refInnerFunc() cúng có thể sử dụng được.
refInnerFunc() sẽ tham chiếu đến môi trường Lexical và hàm innerFunc().
Ứng dụng của Closure trong thực tế là gì?
Trong thực tế, ta sẽ thấy Closure có khá nhiều ứng dụng như:
- Tạo thành một Function factory: một hàm tạo ra một hàm khác
- Mô phỏng lại các phạm vi của biến trong lập trình hướng đối tượng.
- Closure Scope Chain
Để hiểu hơn, chúng ta lại đi vào ví dụ về 3 ứng dụng trên nhé!
Function factory
Chúng ta sẽ có code như sau:
function makeExponentiation(x) {
var exponent = x;
return function(y) {
return Math.pow(y, exponent);
}
}
var sqr = makeExponentiation(2);
var sqrt = makeExponentiation(0.5);
console.log('3 squared is ' + sqr(3));
console.log('căn bậc hai của 9 là ' + sqrt(9));
Trong đó, bạn có thể thấy hàm makeExponentiation giống như một Function factory khi có thể tạo ra được những Function tùy vào tham số truyền vào. 2 Closure: sqrt và sqr sở hữu body giống nhau nhưng khác biến tương đương (ENV – Variable Equivalent).
Mô phỏng lại phạm vi của biến trong lập trình hướng đối tượng
Bên trong JavaScript chưa có khái niệm class đúng nghĩa như trong C++ hay những ngôn ngữ lập trình khác. Trong ES6, cuối cùng, khái niệm class trong JavaScript cũng có. Tuy nhiên, về bản chất đây là một phương pháp giả lập/ mô phỏng/ hack bằng cách sử dụng Closure. Bạn có thể xem code sau đây:
function Counter() {
var counter = 0;
function add(number) {
counter += number;
}
return {
increment: function() {
add(1);
},
decrement: function() {
add(-1);
},
value: function() {
return counter;
}
};
});
var counter = Counter();
console.log('giá trị ban đầu’ + counter.value());
counter.increment();
counter.increment();
console.log('sau khi tăng lên 1' + counter.value());
counter.decrement();
console.log('sau khi giảm xuống 1 ' + counter.value());
Trong đó, các hàm increment, decrement và value đều là Closure có body khác nhau nhưng chia sẻ cùng một Variable Equivalent.
Chia sẻ chung Variable Equivalent chính là “bí quyết” mô phỏng class trong JavaScript. Khi Closure update một biến, việc này đổi này cũng sẽ được ghi nhận ở các Closure khác.
Closure Scope Chain
Tiếp theo, chúng ta sẽ giải thích tiếp về các ý trong phần mở đầu đã đề cập về 3 phạm vi Closure có thể truy cập bao gồm:
- Local Scope: phạm vị tự có
- Outer Functions Scope: phạm vi bên ngoài (phạm vi của hàm cha)
- Global Scope
Khi lập trình, một trong những sai lầm phổ biến là không nhận ra trường hợp Outer Function là một hàm lồng, dẫn đến việc: phạm vi của Outer Function là phạm vi của Outer Function. Điều này dẫn đến một chuỗi những phạm vi hàm rất hiệu quả.
Để chứng minh cho điều này, hãy cùng Tino Group xem xét một ví dụ sau đây nhé!
// đây là 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
// Bạn cũng có thể viết mà không cần các hàm ẩn danh:
// 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 sum2 = sum(1);
var sum3 = sum2(2);
var sum4 = sum3(3);
var result = sum4(4);
console.log(result) //log 20
Trong ví dụ trên, bạn có thể thấy một loạt các hàm lồng nhau. Tất cả những hàm này đều có thể truy cập vào phạm vi của những hàm bên ngoài. Với trường hợp này, chúng ta có thể nói rằng: Closure có khả năng truy cập vào tất cả phạm vi của Outer Function.
Cân nhắc về hiệu suất
Đây không phải là một chức năng của Closure trong thực tế mà là một nhắc nhở. Do phần này quá ngắn nên Tino Group xin phép gộp chung vào ví dụ thay vì tách riêng vì nhắc nhở này có liên quan mật thiết đến ví dụ ở trên.
Việc tạo quá nhiều hàm trong một hàm khác khi Closure không cần thiết để giải quyết một tác vụ cụ thể. Lý do là vì Closure sẽ có thể ảnh hưởng tiêu cực đến hiệu suất của script cũng như cả tốc độ xử lý lẫn mức tiêu thụ bộ nhớ!
Vì vậy, bạn hãy cân nhắc thật kỹ nhé!
Đến đây, Tino Group hi vọng rằng bạn đã hiểu Closure là gì cũng như hiểu hơn về Closure trong JavaScript thông qua các ví dụ. Chúc bạn sẽ thành công trên con đường lập trình đầy gian nan và thử thách nhé!
Bài viết có tham khảo từ nhiều nguồn: Dev.to, Medium, JavaScript Tutorial, Mozilla Developer, TutorialsTeacher, TopDev,…
Những câu hỏi thường gặp về Closure
Tìm hiểu thêm về Closure ở đâu?
Có khá nhiều tài liệu để bạn tham khảo về Closure nói riêng và JavaScript nói chung như:
Dĩ nhiên, tài liệu của Mozilla Developer sẽ có phần chuẩn chỉnh hơn những tài liệu khác nhưng cũng hơi khó hiểu hơn. Bạn sẽ cần phải có tiếng Anh chuyên ngành hoặc ít nhất tiếng Anh để hiểu bài viết của Mozilla Developer.
Ví dụ thường gặp về Closure khi phỏng vấn xin việc là gì?
Ví dụ này được lấy từ bài viết của Giang Coffee trên Medium, bạn có thể tham khảo, thử tìm ra lỗi sai và khắc phục lỗi này nhé!
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000)
}
Cách khắc phục lỗi sai trong ví dụ trên là gì?
Cách thứ nhất: sử dụng let để giải quyết lỗi.
Cách thứ hai: sử dụng thêm nhiều Closure nữa, ví dụ như:
for (var i = 0; i < 3; i++) {
function log(x) {
return function() {
console.log(x);
}
}
setTimeout(log(i), 1000)
}
Dĩ nhiên, trong bài phỏng vấn bạn nhớ được gì bạn cứ thử nhé!
Vì sao trả lời được câu hỏi về Closure nhưng vẫn bị đánh trượt phỏng vấn?
Có vẻ, bạn đã quay lại bài viết này sau một thời gian đi phỏng vấn, dù trả lời được câu hỏi về Closure do nhà tuyển dụng lấy đúng ví dụ trên mạng đúng không nào?
Đừng buồn, có thể vì môi trường không phù hợp, bạn không phù hợp hoặc nhà tuyển dụng thấy mức kỹ năng của bạn vẫn chưa đủ và khá nhiều lý do khác nè.