Closure là một trong những yếu tố cốt lõi tạo nên sức mạnh của JavaScript. Tuy nhiên, chính sự linh hoạt của nó lại khiến nhiều lập trình viên cảm thấy khó nắm bắt. Trong bài viết này, bạn sẽ được giải thích một cách rõ ràng và dễ hiểu về Closure, cách nó hoạt động, cùng những lợi ích và tình huống ứng dụng phổ biến nhất.
Bạn có thể tìm hiểu rõ hơn về Closure tại đây: Closure là gì trong JavaScript? Chi tiết A-Z + Ví dụ dễ hiểu
Closure là gì?
Closure trong JavaScript, closure (đóng) là một hàm có quyền truy cập vào các biến của hàm bên ngoài, ngay cả sau khi hàm bên ngoài đã hoàn thành việc thực hiện. Điều này giúp giữ trạng thái của các biến đó, tạo ra những hàm mạnh mẽ và bảo vệ dữ liệu.
Để dễ hình dung, hãy xem xét một hàm được định nghĩa bên trong một hàm khác. Hàm bên trong có thể truy cập các biến cục bộ của hàm bên ngoài. Đây là cơ chế cơ bản hình thành nên Closure.
Cách hoạt động của Closure
Cơ chế hoạt động của Closure dựa trên khái niệm Lexical Scoping (phạm vi từ vựng). Lexical Scoping xác định phạm vi của một biến dựa trên vị trí nó được khai báo trong mã nguồn. Khi một hàm được định nghĩa, nó “ghi nhớ” môi trường mà nó được tạo ra, bao gồm tất cả các biến có sẵn trong phạm vi đó.
Khi hàm bên ngoài thực thi và trả về hàm bên trong, hàm bên trong vẫn duy trì liên kết với các biến của hàm cha. Điều này cho phép hàm bên trong tiếp tục sử dụng các biến đó, ngay cả khi hàm cha không còn chạy nữa. Các biến này không bị hủy bỏ khỏi bộ nhớ.
Ví dụ về Closure
Xem xét ví dụ sau để hiểu rõ hơn cách Closure hoạt động:
JavaScript
function taoBoDem() {
let count = 0; // Biến ‘count’ nằm trong phạm vi của taoBoDem
return function tangBoDem() {
count++; // tangBoDem có quyền truy cập ‘count’ từ phạm vi bên ngoài
console.log(count);
};
}
const boDem1 = taoBoDem();
boDem1(); // Output: 1
boDem1(); // Output: 2
const boDem2 = taoBoDem();
boDem2(); // Output: 1
Trong ví dụ trên, tangBoDem là một Closure. Nó “ghi nhớ” biến count từ hàm taoBoDem . Mỗi khi taoBoDem được gọi, một môi trường count mới được tạo ra. boDem1 và boDem2 là các instance riêng biệt của Closure, mỗi cái giữ một count riêng.
Một ví dụ khác minh họa việc duy trì trạng thái:
JavaScript
function taoHamChao(ten) {
return function() {
console.log(Chào ${ten}!
);
};
}
const chaoLan = taoHamChao(‘Lan’);
chaoLan(); // Output: Chào Lan!
const chaoHung = taoHamChao(‘Hùng’);
chaoHung(); // Output: Chào Hùng!
Ở đây, hàm bên trong truy cập biến ten của hàm bên ngoài taoHamChao . Mỗi hàm được trả về sẽ “ghi nhớ” giá trị ten riêng của nó.
Tại sao Closure lại quan trọng?
Closure là một công cụ mạnh mẽ trong JavaScript vì nó cho phép các hàm “ghi nhớ” môi trường của chúng. Điều này mở ra nhiều khả năng thiết kế code linh hoạt và hiệu quả. Closure giúp tạo ra các hàm có trạng thái riêng biệt và cô lập, không ảnh hưởng đến các phần khác của chương trình.
Nó đặc biệt hữu ích trong việc xây dựng các module, quản lý state, và tạo ra các hàm tùy chỉnh. Sự linh hoạt này giúp giảm thiểu lỗi và tăng khả năng tái sử dụng code. Hiểu Closure là bước đệm quan trọng để làm chủ JavaScript.
Các trường hợp ứng dụng của Closure
Closure có nhiều ứng dụng thực tế trong JavaScript hiện đại. Dưới đây là một số trường hợp phổ biến:
Data Privacy/Encapsulation (Bảo mật dữ liệu/Đóng gói)
Closure cho phép bạn tạo ra các biến “riêng tư” (private variables) mà không thể truy cập trực tiếp từ bên ngoài. Điều này hỗ trợ nguyên tắc đóng gói (encapsulation) trong lập trình hướng đối tượng. Chỉ các hàm được định nghĩa bên trong Closure mới có quyền truy cập và thay đổi giá trị của các biến đó.
JavaScript
function taoTaiKhoan(soDuBanDau) {
let soDu = soDuBanDau;
return {
getSoDu: function() {
return soDu;
},
napTien: function(soTien) {
if (soTien > 0) {
soDu += soTien;
console.log(Nạp ${soTien}. Số dư hiện tại: ${soDu}
);
}
},
rutTien: function(soTien) {
if (soTien > 0 && soTien <= soDu) {
soDu -= soTien;
console.log(Rút ${soTien}. Số dư hiện tại: ${soDu}
);
} else {
console.log(“Số dư không đủ hoặc số tiền không hợp lệ.”);
}
}
};
}
const taiKhoan = taoTaiKhoan(100);
console.log(taiKhoan.getSoDu()); // Output: 100
taiKhoan.napTien(50); // Output: Nạp 50. Số dư hiện tại: 150
taiKhoan.rutTien(30); // Output: Rút 30. Số dư hiện tại: 120
// console.log(taiKhoan.soDu); // Undefined - soDu là biến riêng tư
Trong ví dụ này, soDu là biến riêng tư. Nó chỉ có thể được truy cập thông qua các phương thức getSoDu, napTien, rutTien được trả về bởi taoTaiKhoan.
Function Factories (Hàm tạo hàm)
Closure cho phép bạn tạo ra các hàm “tùy chỉnh” dựa trên các tham số đầu vào. Một hàm cha có thể trả về một hàm con được cấu hình sẵn. Đây là một pattern mạnh mẽ để tạo ra các hàm có hành vi chuyên biệt.
JavaScript
function taoBoLocTheoGia(giaToiThieu) {
return function(sanPham) {
return sanPham.gia >= giaToiThieu;
};
}
const locSanPhamGiaCao = taoBoLocTheoGia(500);
const sanPham = [
{ ten: ‘Laptop’, gia: 1200 },
{ ten: ‘Chuột’, gia: 50 },
{ ten: ‘Bàn phím’, gia: 150 },
{ ten: ‘Điện thoại’, gia: 700 }
];
const sanPhamLoc = sanPham.filter(locSanPhamGiaCao);
console.log(sanPhamLoc);
// Output: [ { ten: ‘Laptop’, gia: 1200 }, { ten: ‘Điện thoại’, gia: 700 } ]
taoBoLocTheoGia là một function factory. Nó tạo ra các hàm lọc sản phẩm khác nhau dựa trên giá tối thiểu được cung cấp.
Memorization/Caching (Ghi nhớ/Bộ nhớ đệm)
Closure có thể được sử dụng để triển khai kỹ thuật memorization, nơi kết quả của một hàm được lưu trữ sau lần tính toán đầu tiên. Điều này giúp tối ưu hiệu suất bằng cách tránh tính toán lại cho cùng một đầu vào.
JavaScript
function taoBoNhoDemTong() {
const cache = {};
return function(n) {
if (cache[n]) {
console.log(Lấy từ cache cho ${n}
);
return cache[n];
}
console.log(Tính toán cho ${n}
);
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += i;
}
cache[n] = sum;
return sum;
};
}
const tinhTong = taoBoNhoDemTong();
console.log(tinhTong(5)); // Tính toán cho 5, Output: 15
console.log(tinhTong(5)); // Lấy từ cache cho 5, Output: 15
console.log(tinhTong(10)); // Tính toán cho 10, Output: 55
Biến cache được duy trì bởi Closure tinhTong, cho phép nó ghi nhớ các kết quả đã tính toán trước đó.
Currying (Curry hóa hàm)
Currying là một kỹ thuật biến đổi hàm, nơi một hàm nhận nhiều đối số được chuyển đổi thành một chuỗi các hàm, mỗi hàm nhận một đối số duy nhất. Closure đóng vai trò quan trọng trong việc giữ lại các đối số đã được truyền.
JavaScript
function add(a) {
return function(b) {
return a + b;
};
}
const addFive = add(5);
console.log(addFive(3)); // Output: 8
console.log(add(10)(20)); // Output: 30
add(a) trả về một hàm Closure ghi nhớ giá trị a, sau đó đợi giá trị b để thực hiện phép cộng.
Callbacks và Event Handlers
Trong lập trình bất đồng bộ hoặc xử lý sự kiện, Closure thường được sử dụng để duy trì ngữ cảnh hoặc dữ liệu khi một hàm được gọi sau.
JavaScript
function guiThongBao(tinNhan) {
setTimeout(function() {
console.log(Thông báo: ${tinNhan}
);
}, 1000);
}
guiThongBao(“Đã hoàn tất thao tác!”);
// Sau 1 giây, Output: Thông báo: Đã hoàn tất thao tác!
Hàm callback của setTimeout là một Closure, nó vẫn có thể truy cập biến tinNhan ngay cả khi guiThongBao đã kết thúc thực thi.
Sự khác biệt giữa Closure và Scope
Scope (Phạm vi) là cơ chế xác định khả năng truy cập của các biến và hàm trong code. Trong JavaScript, có hai loại scope chính: Global Scope (phạm vi toàn cục) và Local (Function) Scope (phạm vi cục bộ/hàm). Các biến được định nghĩa trong một scope chỉ có thể được truy cập trong scope đó hoặc các scope con của nó.
Closure là một hệ quả của Lexical Scoping. Closure xảy ra khi một hàm bên trong duy trì quyền truy cập vào các biến của hàm bên ngoài, ngay cả sau khi hàm bên ngoài đã thực thi xong. Nói cách khác, scope định nghĩa nơi một biến có thể được truy cập, còn Closure là hiện tượng một hàm “mang theo” scope môi trường nơi nó được tạo ra.
Mọi Closure đều liên quan đến scope, nhưng không phải mọi scope đều tạo ra Closure. Closure chỉ hình thành khi một hàm con được trả về hoặc truyền đi, và nó vẫn giữ liên kết với các biến của môi trường cha nó.
Lưu ý khi sử dụng Closure
Mặc dù Closure mạnh mẽ, việc sử dụng không đúng cách có thể dẫn đến một số vấn đề:
Rò rỉ bộ nhớ (Memory Leaks): Nếu một Closure giữ tham chiếu đến các đối tượng lớn mà không bao giờ được giải phóng, nó có thể gây ra rò rỉ bộ nhớ. Điều này xảy ra khi Closure ngăn chặn garbage collector dọn dẹp các tài nguyên không còn cần thiết. Cần cẩn trọng khi các biến được giữ lại bởi Closure không còn được sử dụng.
Hiệu suất: Việc tạo ra nhiều Closure có thể ảnh hưởng đến hiệu suất, đặc biệt là trong các vòng lặp lớn. Mỗi Closure đều có một bản sao riêng của môi trường từ vựng. Cần cân nhắc tối ưu hóa trong các trường hợp nhạy cảm về hiệu suất.
Khó Debug: Đôi khi, việc theo dõi giá trị của các biến trong Closure có thể phức tạp. Debugging có thể trở nên khó khăn hơn do các biến được truy cập từ một scope không còn hoạt động. Sử dụng các công cụ debug của trình duyệt một cách hiệu quả sẽ giúp ích.
Câu hỏi thường gặp về Closure trong phỏng vấn
Khi phỏng vấn cho vị trí lập trình JavaScript, Closure là một chủ đề thường được hỏi. Dưới đây là một số câu hỏi phổ biến:
Closure là gì?
Trả lời: Closure là một hàm ghi nhớ và truy cập các biến từ phạm vi bên ngoài của nó, ngay cả sau khi hàm bên ngoài đã kết thúc.
Giải thích Lexical Scoping liên quan đến Closure.
Trả lời: Lexical Scoping là cách một hàm được xác định bởi vị trí nó được viết. Closure tận dụng điều này bằng cách “ghi nhớ” môi trường từ vựng của nó.
Khi nào nên sử dụng Closure?
Trả lời: Sử dụng Closure khi cần bảo mật dữ liệu (biến riêng tư), tạo hàm nhà máy, ghi nhớ kết quả, hoặc khi làm việc với callbacks.
Có bất kỳ nhược điểm nào khi sử dụng Closure không?
Trả lời: Nhược điểm tiềm ẩn bao gồm rò rỉ bộ nhớ và ảnh hưởng đến hiệu suất nếu sử dụng không cẩn thận.
Phân biệt Closure và Scope.
Trả lời: Scope là quy tắc hiển thị biến, còn Closure là một hiện tượng nơi hàm con duy trì quyền truy cập vào các biến của scope cha.
Closure là một khái niệm cơ bản nhưng vô cùng mạnh mẽ trong JavaScript, cho phép bạn viết code linh hoạt, bảo mật và hiệu quả hơn. Việc hiểu rõ cách Closure hoạt động, các trường hợp ứng dụng, và những lưu ý khi sử dụng sẽ giúp bạn trở thành một lập trình viên JavaScript thành thạo hơn. Thực hành và áp dụng các ví dụ trên vào các dự án của riêng bạn là cách tốt nhất để nắm vững khái niệm này.