- Closure là gì? Định nghĩa ngắn gọn
- Ví dụ nhanh: bộ đếm có trạng thái riêng
- Vì sao closure hữu ích?
- Ba mẫu code thực dụng dùng closure
- 1. once: chỉ chạy hàm một lần
- 2. partial: ghim sẵn một phần tham số
- 3. memoize: nhớ kết quả theo key
- Closure, scope và var/let/const
- Bất đồng bộ và closure: setTimeout, event, Promise
- Rò rỉ bộ nhớ vì closure? Hiểu cho đúng
- Phân biệt this và closure
- Debug và kiểm thử closure
- Các lỗi phổ biến cần tránh
- Tóm tắt nhanh
Closure là gì? Định nghĩa ngắn gọn
Mỗi khi JavaScript tạo một hàm, nó gắn kèm “môi trường từ vựng” (lexical environment) chứa các biến ở thời điểm khai báo. Khi hàm được gọi ở bất kỳ đâu sau này, nó vẫn có thể truy cập các biến đó. Tập hợp giữa hàm và môi trường “đi kèm” ấy được gọi là closure.
Ví dụ nhanh: bộ đếm có trạng thái riêng
function createCounter(start = 0) {
let count = start; // biến trong phạm vi từ vựng
return {
inc() { count++; return count; },
dec() { count--; return count; },
get() { return count; }
};
}
const c1 = createCounter(10);
c1.inc(); // 11
c1.get(); // 11
const c2 = createCounter(); // c2 độc lập với c1
c2.inc(); // 1
Mỗi đối tượng trả về “đóng” trên biến count riêng của nó.
Vì sao closure hữu ích?
- Đóng gói và ẩn dữ liệu: tạo “biến private” mà không cần class.
- API gọn gàng: trả về hàm đã gắn cấu hình trước (currying/partial).
- Callback “nhớ” ngữ cảnh: dùng trong event, timer, Promise.
- Tối ưu hiệu năng: ghi nhớ kết quả tính toán (memoization).
Ba mẫu code thực dụng dùng closure
1. once: chỉ chạy hàm một lần
function once(fn) {
let called = false, value;
return function(...args) {
if (!called) { value = fn.apply(this, args); called = true; }
return value;
};
}
const init = once(() => { /* khởi tạo tốn kém */ return "ok"; });
init(); // chạy thật
init(); // dùng lại kết quả
2. partial: ghim sẵn một phần tham số
function partial(fn, ...preset) {
return (...later) => fn(...preset, ...later);
}
const add = (a,b,c) => a+b+c;
const add5 = partial(add, 2, 3);
add5(10); // 15
3. memoize: nhớ kết quả theo key
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const val = fn.apply(this, args);
cache.set(key, val);
return val;
};
}
const slowFib = n => n < 2 ? n : slowFib(n-1) + slowFib(n-2);
const fastFib = memoize(slowFib);
fastFib(40); // nhanh hơn đáng kể ở các lần sau
Closure, scope và var/let/const
Closure phụ thuộc vào phạm vi từ vựng. Điểm hay gặp lỗi nhất là vòng lặp với var vì var có scope theo function, không theo block.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// In ra: 3,3,3
Cách đúng là dùng let (block scope) hoặc IIFE để “chốt” giá trị mỗi vòng:
// Cách 1: let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0,1,2
}
// Cách 2: IIFE với var
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 0);
})(i);
}
Bất đồng bộ và closure: setTimeout, event, Promise
Closure giúp callback “nhớ” biến đúng tại thời điểm khai báo, miễn là bạn dùng đúng scope.
function countdown(from) {
for (let i = from; i >= 0; i--) {
setTimeout(() => console.log(i), (from - i) * 1000);
}
}
countdown(3); // 3,2,1,0 theo từng giây
Rò rỉ bộ nhớ vì closure? Hiểu cho đúng
- Closure không tự gây rò rỉ. GC vẫn thu gom nếu không còn tham chiếu đến hàm/biến.
- Dễ rò rỉ khi vô tình giữ tham chiếu dài hạn, ví dụ lưu callback vào một mảng global, hoặc gắn listener vào DOM mà không tháo.
- Tháo listener khi không cần, tránh giữ cache vô hạn, và đặt thời hạn cho dữ liệu lưu trong closure.
// Ví dụ: nhớ tháo listener
function bind(el) {
function onClick() { /* dùng closure */ }
el.addEventListener('click', onClick);
return () => el.removeEventListener('click', onClick);
}
Phân biệt this và closure
- Closure “giữ” biến theo phạm vi từ vựng, không “giữ”
this. thisphụ thuộc ngữ cảnh gọi hàm. Arrow function không cóthisriêng, nó dùngthiscủa phạm vi ngoài gần nhất.
const obj = {
x: 42,
normal() { setTimeout(function(){ console.log(this.x); }, 0); }, // undefined
arrow() { setTimeout(() => { console.log(this.x); }, 0); } // 42
};
obj.normal();
obj.arrow();
Debug và kiểm thử closure
- Dùng DevTools Sources → Watch/Scope để xem biến trong Lexical environment.
- Đặt breakpoint ngay trong hàm được trả về hoặc callback.
- Kiểm thử hành vi: gọi nhiều lần và xác nhận trạng thái không bị lộ hoặc bị ghi đè sai.
// Kiểm thử đơn giản
const counter = (function() {
let n = 0;
return () => ++n;
})();
console.assert(counter() === 1);
console.assert(counter() === 2);
Các lỗi phổ biến cần tránh
- Vòng lặp với
vardẫn đến tất cả callback chia sẻ cùng một biến. - Giữ state trong closure quá lâu khiến khó quản lý vòng đời và test.
- Memoize không có chiến lược xóa cache, làm phình bộ nhớ.
- Nhầm lẫn giữa closure và ràng buộc
this, dẫn đến giá trịthissai.
Tóm tắt nhanh
- Closure = hàm + môi trường từ vựng tại thời điểm định nghĩa.
- Dùng để ẩn dữ liệu, cấu hình trước hàm, quản lý callback bất đồng bộ, và tối ưu hiệu năng.
- Ưu tiên
let/constđể tránh bẫy scope; quản lý vòng đời listener/cache để không rò rỉ. - Phân biệt rõ closure với
this; arrow function giúp “ghim”thistheo lexical scope.
Bình luận