Mục lục
Xem mục lục
Closure là gì
Closure là khả năng một function “nhớ” được biến ở scope bao ngoài (outer scope), ngay cả khi scope đó đã kết thúc thực thi. Nói cách khác, inner function vẫn giữ được quyền truy cập vào biến của outer function, dù outer function đã return từ lâu.
Lexical Scope, nền tảng của Closure
Closure hình thành dựa trên Lexical Scope: phạm vi truy cập biến của một function được xác định bởi nơi function đó được khai báo trong code, không phải nơi nó được gọi.
function outer() {
const message = "Xin chào";
function inner() {
console.log(message);
}
return inner;
}
inner được khai báo bên trong outer, nên theo lexical scope, inner luôn có quyền truy cập vào biến của outer, bất kể inner được gọi ở đâu sau này.
Ví dụ minh họa cơ bản
function outer() {
const message = "Xin chào";
function inner() {
console.log(message);
}
return inner;
}
const fn = outer();
fn(); // "Xin chào"
outer() đã chạy xong và return trước khi fn() được gọi. Bình thường, biến cục bộ message sẽ bị hủy khi function chứa nó kết thúc. Nhưng vì inner (được gán cho fn) tham chiếu tới message, JavaScript giữ lại biến này thay vì hủy nó. Đây chính là closure.
Vì sao biến không bị garbage collector dọn
Garbage collector chỉ dọn một vùng nhớ khi không còn bất kỳ tham chiếu nào trỏ tới nó. Closure giữ tham chiếu trực tiếp đến biến, không phải bản sao giá trị tại thời điểm tạo.
function counter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const increment = counter();
console.log(increment()); // 1
console.log(increment()); // 2
Mỗi lần gọi increment(), giá trị count được cập nhật và giữ nguyên cho lần gọi tiếp theo, vì closure tham chiếu tới cùng một biến count, không tạo bản sao mới mỗi lần gọi.
Cạm bẫy thường gặp: closure trong vòng lặp với var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// In ra: 3, 3, 3
Nguyên nhân: var có function scope, không có block scope, nên chỉ tồn tại một biến i duy nhất dùng chung cho cả vòng lặp. Khi các callback trong setTimeout thực thi (sau khi vòng lặp đã chạy xong), chúng đều tham chiếu tới cùng biến i, lúc này đã có giá trị 3.
Cách khắc phục bằng let, vì let có block scope, mỗi vòng lặp tạo ra một biến i riêng:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// In ra: 0, 1, 2
Trước khi có let (ES6), cách khắc phục phổ biến là dùng IIFE (Immediately Invoked Function Expression) để tạo scope riêng cho mỗi vòng lặp:
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(() => console.log(j), 0);
})(i);
}
// In ra: 0, 1, 2
Ứng dụng thực tế
Data privacy / Module pattern
Closure cho phép tạo biến “private”, không thể truy cập trực tiếp từ bên ngoài, chỉ thao tác được qua các function được expose:
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
return balance;
},
getBalance() {
return balance;
},
};
}
const account = createBankAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 150
console.log(account.balance); // undefined, không truy cập trực tiếp được
Memoization
Closure giữ lại cache giữa các lần gọi, tránh tính toán lại kết quả đã có:
function memoize(fn) {
const cache = new Map();
return function (arg) {
if (cache.has(arg)) {
return cache.get(arg);
}
const result = fn(arg);
cache.set(arg, result);
return result;
};
}
Currying
Closure cho phép một function trả về function khác, giữ lại các tham số đã truyền trước đó:
function multiply(a) {
return function (b) {
return a * b;
};
}
const double = multiply(2);
console.log(double(5)); // 10
Debounce / Throttle
Closure giữ lại trạng thái (timer, thời điểm gọi gần nhất) giữa các lần gọi function:
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
Đánh đổi cần lưu ý
Closure giữ tham chiếu biến trong suốt thời gian còn được sử dụng, nghĩa là biến đó không được giải phóng bộ nhớ sớm. Trong phần lớn trường hợp điều này không đáng lo, nhưng có thể gây memory leak nếu:
- Closure giữ tham chiếu tới DOM node đã bị gỡ khỏi cây DOM, nhưng vẫn còn được closure tham chiếu (ví dụ trong event listener chưa được gỡ đúng cách), khiến DOM node đó không được garbage collector dọn.
- Closure giữ tham chiếu tới object lớn không còn cần thiết, nhưng vẫn tồn tại vì một function nào đó (có thể chỉ dùng một phần nhỏ dữ liệu) vẫn còn tham chiếu tới toàn bộ scope chứa object đó.
Cách giảm rủi ro: gỡ event listener khi không còn cần (removeEventListener), tránh giữ closure sống lâu hơn mức cần thiết, và tách riêng biến thực sự cần giữ lại thay vì để closure tham chiếu tới cả một scope lớn.
Tổng kết
- Closure là khả năng function nhớ được biến ở outer scope, dựa trên lexical scope tại thời điểm khai báo.
- Closure giữ tham chiếu trực tiếp đến biến, không phải bản sao giá trị, nên biến được cập nhật và giữ nguyên qua các lần gọi.
- Vòng lặp dùng
varlà cạm bẫy kinh điển do function scope, khắc phục bằnglethoặc IIFE. - Ứng dụng thực tế phổ biến: module pattern (private state), memoization, currying, debounce/throttle.
- Closure giữ biến sống lâu hơn bình thường, cần lưu ý tránh giữ tham chiếu không cần thiết để hạn chế memory leak.