Trong hệ sinh thái JavaScript, có những khái niệm bạn có thể lướt qua, nhưng cũng có những “tảng đá tảng” bạn buộc phải chinh phục nếu muốn viết code sạch, bảo mật và hiệu quả. Closure chính là tảng đá lớn nhất trong số đó.

Nếu bạn từng tự hỏi tại sao biến trong hàm setTimeout vẫn giữ được giá trị cũ, hay làm thế nào để tạo ra các biến “private” trong ngôn ngữ không hỗ trợ class truyền thống, thì Closure là gì chính là câu trả lời bạn đang tìm kiếm. Bài viết này sẽ không chỉ đưa ra định nghĩa khô khan, mà sẽ mổ xẻ tận cùng cơ chế hoạt động của bộ nhớ, Lexical Scope và các ứng dụng thực tế.
Định nghĩa Closure là gì? (Hiểu theo cách đơn giản nhất)
Trước khi đi vào định nghĩa học thuật, hãy tưởng tượng một ví dụ đời thường:
Hãy tưởng tượng bạn có một chiếc balo. Khi bạn rời khỏi nhà (hàm cha), bạn mang theo chiếc balo đó. Dù bạn đi đến bất cứ đâu (hàm con được thực thi ở nơi khác), bạn vẫn có thể lấy đồ đạc trong balo ra dùng. Những đồ đạc đó chính là các biến từ “ngôi nhà” cũ của bạn.

Định nghĩa kỹ thuật: Theo tài liệu chính thức từ MDN: Closure là sự kết hợp giữa một hàm và môi trường từ vựng (lexical environment) nơi hàm đó được khai báo.
Nói một cách dễ hiểu hơn cho dân lập trình: Closure cho phép một hàm con (inner function) truy cập và ghi nhớ các biến của hàm cha (outer function), ngay cả khi hàm cha đã thực thi xong và bị loại bỏ khỏi Call Stack.
Tại sao khái niệm này lại quan trọng?
Quản lý bộ nhớ: Hiểu khi nào biến được giải phóng.
Bảo mật dữ liệu: Tạo ra tính đóng gói (Encapsulation).
Functional Programming: Là nền tảng của Currying và Higher-Order Functions.
Nền tảng cốt lõi: Lexical Scope (Phạm vi từ vựng)
Để hiểu Closure, bạn bắt buộc phải hiểu Lexical Scope. Đây là quy tắc xác định xem một biến có thể được truy cập ở đâu trong code.
Trong JavaScript, phạm vi (scope) được xác định bởi vị trí bạn viết code (thời điểm define), chứ không phải nơi bạn gọi hàm (thời điểm runtime).
Quy tắc “Nesting Dolls” (Búp bê Nga)
Hãy xem scope như những con búp bê Nga lồng vào nhau:
Hàm con nhìn thấy biến của chính nó.
Hàm con nhìn thấy biến của hàm cha.
Hàm con nhìn thấy biến toàn cục (Global).
Ngược lại, hàm cha không thể nhìn thấy biến của hàm con.
JavaScript
function ongNoi() {
const taiSanOng = "Nhà Cổ";
function cha() {
const taiSanCha = "Xe Hơi";
console.log(taiSanOng); // Được phép: Scope cha
function con() {
const taiSanCon = "Laptop";
console.log(taiSanCha); // Được phép: Scope cha của cha
console.log(taiSanOng); // Được phép: Scope ông nội
}
con();
}
cha();
}
ongNoi();Trong ví dụ trên, hàm con có quyền truy cập vào scope của cha và ongNoi nhờ vào Lexical Scope. Đây chính là tiền đề để Closure tồn tại.
Cơ chế hoạt động của Closure (Under the Hood)
Đây là phần mà hầu hết các hướng dẫn cơ bản bỏ qua, nhưng là phần quan trọng nhất để bạn trở thành chuyên gia.

Khi một hàm được thực thi, nó tạo ra một Execution Context (Ngữ cảnh thực thi) và được đẩy vào Call Stack. Thông thường, khi hàm chạy xong (gặp từ khóa return hoặc đóng ngoặc }), Execution Context này sẽ bị “pop” ra khỏi Stack, và các biến cục bộ sẽ bị Garbage Collector (Bộ dọn rác) xóa bỏ để giải phóng bộ nhớ.
Nhưng điều kỳ diệu xảy ra với Closure: Nếu hàm cha trả về một hàm con, và hàm con đó đang sử dụng biến của hàm cha, JavaScript sẽ không xóa các biến đó.
Thay vào đó, JavaScript giữ lại các biến được tham chiếu này trong một vùng nhớ riêng (thường nằm trong Heap) gọi là Closure Scope. Hàm con sẽ mang theo một tham chiếu ẩn (thường gọi là [[Environment]]) trỏ về Closure Scope này.
Ví dụ minh họa:
function taoTaiKhoan(soDuBanDau) {
let soDu = soDuBanDau; // Biến cục bộ của hàm cha
return function rutTien(soTien) {
if (soTien > soDu) {
console.log("Không đủ tiền!");
} else {
soDu -= soTien;
console.log(`Đã rút ${soTien}, còn lại: ${soDu}`);
}
};
}
const taiKhoanCuaToi = taoTaiKhoan(1000);
// Lúc này taoTaiKhoan đã chạy xong và rời khỏi Call Stack.
// Theo lý thuyết thông thường, biến 'soDu' phải bị hủy.
taiKhoanCuaToi(200); // Output: Đã rút 200, còn lại: 800
taiKhoanCuaToi(100); // Output: Đã rút 100, còn lại: 700Giải thích: Hàm taiKhoanCuaToi chính là hàm rutTien. Dù taoTaiKhoan đã kết thúc, taiKhoanCuaToi vẫn giữ một “sợi dây liên kết” đến biến soDu. Biến soDu này không thể bị truy cập trực tiếp từ bên ngoài, tạo nên cơ chế bảo mật tuyệt vời.
Ứng dụng thực tế của Closure trong Lập trình
Học lý thuyết là chưa đủ. Dưới đây là các trường hợp cụ thể (Use Cases) mà bạn sẽ sử dụng Closure trong các dự án thực tế.

Data Encapsulation (Giả lập biến Private)
Trong lập trình hướng đối tượng (OOP) như Java hay C#, chúng ta có private. Trước khi ES6 Class ra đời (và thậm chí cả bây giờ), Closure là cách duy nhất để che giấu dữ liệu.
JavaScript
const Counter = (function() {
let count = 0; // Biến private, bên ngoài không thể sửa đổi trực tiếp
return {
increment: function() {
count++;
console.log(count);
},
decrement: function() {
count--;
console.log(count);
},
value: function() {
return count;
}
};
})();
Counter.increment(); // 1
Counter.increment(); // 2
// Counter.count = 100; // Lỗi hoặc không có tác dụng
console.log(Counter.value()); // 2Đây được gọi là Module Pattern, giúp code an toàn hơn, tránh việc các biến bị thay đổi ngẫu nhiên từ các phần khác của chương trình.
Function Factories (Nhà máy tạo hàm) và Currying
Closure cho phép bạn tạo ra các hàm mới dựa trên cấu hình ban đầu.
JavaScript
function makeMultiplier(multiplier) {
return function(x) {
return x * multiplier;
};
}
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15Kỹ thuật này cực kỳ hữu ích trong Functional Programming và khi làm việc với các hàm xử lý sự kiện (Event Handlers) cần tham số động.
Memoization (Tối ưu hóa hiệu năng)
Memoization là kỹ thuật ghi nhớ kết quả của một hàm để không phải tính toán lại nếu đầu vào không đổi. Closure đóng vai trò là “bộ nhớ cache”.
JavaScript
function memoizedAdd() {
let cache = {}; // Closure giữ biến cache này sống mãi
return function(n) {
if (n in cache) {
console.log("Lấy từ cache...");
return cache[n];
} else {
console.log("Đang tính toán...");
let result = n + 10;
cache[n] = result;
return result;
}
}
}
const add = memoizedAdd();
console.log(add(5)); // Đang tính toán... 15
console.log(add(5)); // Lấy từ cache... 15Cạm bẫy kinh điển: Closure trong vòng lặp (Loops)
Đây là câu hỏi phỏng vấn “huyền thoại” mà 90% ứng viên Junior đều vấp phải.

Vấn đề:
JavaScript
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}Kỳ vọng: In ra 1, 2, 3 sau 1 giây. Thực tế: In ra 4, 4, 4.
Tại sao? Hàm setTimeout là bất đồng bộ (asynchronous). Khi nó chạy, vòng lặp for đã chạy xong từ lâu và giá trị của i đã tăng lên thành 4 (điều kiện dừng là i <= 3, nên khi i=4 vòng lặp dừng). Do sử dụng var, biến i có function scope (hoặc global scope), nghĩa là cả 3 hàm callback trong setTimeout đều trỏ vào cùng một tham chiếu biến i trong bộ nhớ.
Cách khắc phục:
Cách 1: Sử dụng let (ES6) let có block scope. Mỗi lần lặp, JavaScript tạo ra một biến i mới hoàn toàn cho scope đó. Closure sẽ “chụp” lấy giá trị i riêng biệt của từng vòng lặp.
JavaScript
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Output: 1, 2, 3Cách 2: Sử dụng IIFE (Immediately Invoked Function Expression) Tạo ra một scope mới bằng cách gọi hàm ngay lập tức để “khóa” giá trị của i.
JavaScript
for (var i = 1; i <= 3; i++) {
(function(index) {
setTimeout(function() {
console.log(index);
}, 1000);
})(i);
}Closure và Memory Leak (Rò rỉ bộ nhớ)
Mặc dù mạnh mẽ, Closure là con dao hai lưỡi. Vì Closure giữ tham chiếu đến các biến trong hàm cha, nên Garbage Collector không thể dọn dẹp các biến đó miễn là Closure còn tồn tại.

Nếu bạn lạm dụng Closure, đặc biệt là gắn vào các sự kiện toàn cục (như window.addEventListener) hoặc DOM node mà không gỡ bỏ đúng cách, bộ nhớ RAM sẽ bị chiếm dụng ngày càng nhiều, dẫn đến ứng dụng bị chậm hoặc crash trình duyệt.
Lời khuyên:
Chỉ sử dụng Closure khi thực sự cần thiết.
Gán giá trị
nullcho biến chứa Closure khi không còn sử dụng để ngắt tham chiếu, cho phép bộ dọn rác làm việc.
Closure trong kỷ nguyên Modern JavaScript (React Hooks)
Bạn có thể nghĩ “Tôi dùng React, tôi không cần quan tâm cái này”. Sai lầm!

React Hooks (useState, useEffect) hoạt động hoàn toàn dựa trên Closure.
Khi bạn gặp lỗi “Stale Closure” trong React (khi useEffect không cập nhật giá trị mới nhất của state), đó là do Closure đã “chụp” lại giá trị cũ của state tại thời điểm hàm được tạo ra.
Ví dụ lỗi Stale Closure:
JavaScript
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // Luôn in ra 0 vì Closure chụp giá trị count ban đầu
}, 1000);
return () => clearInterval(timer);
}, []); // Dependency array rỗng
// ...
}Hiểu Closure giúp bạn biết cách sửa lỗi này bằng cách thêm count vào dependency array hoặc dùng functional update setCount(prev => prev + 1).
Xem thêm:
- Yup là gì? “Vệ sĩ” đắc lực cho Form Validation trong Javascript (Kèm ví dụ thực tế)
- EJS là gì? Giải mã Embedded JavaScript Template Engine cho Node.js
- Fetch là gì? Hướng dẫn toàn tập về Fetch API trong JavaScript
- Promise là gì? Giải mã “Promise” trong JavaScript từ A-Z
- addEventListener JavaScript là gì? Hướng dẫn toàn tập từ A-Z
Kết luận và Lời khuyên cho Developer
Closure là gì? Tóm lại, đó là khả năng một hàm ghi nhớ nơi nó được sinh ra và những gì thuộc về nơi đó.
Để làm chủ JavaScript, bạn không thể né tránh Closure. Nó xuất hiện ở khắp mọi nơi:
Trong các design pattern.
Trong các thư viện (React, Redux, jQuery).
Trong các câu hỏi phỏng vấn Senior.
Lộ trình luyện tập:
Nắm vững Lexical Scope.
Viết code tạo private variable bằng Closure.
Thử giải thích lỗi vòng lặp
forvàvarcho người khác hiểu.Debug code React Hook và phân tích xem Closure đang hoạt động ở đâu.
Hiểu sâu về Closure không chỉ giúp bạn fix bug nhanh hơn mà còn thay đổi tư duy lập trình của bạn, giúp bạn viết code tinh tế và đẳng cấp hơn.
Văn Tân

Công cụ và hướng dẫn Công nghệ tương lai
ARPANET là gì? Toàn tập Lịch sử & Kiến trúc Kỹ thuật
Nếu bạn đang sử dụng bất kỳ thiết bị nào để đọc bài viết
Xem thêmTh5
Công nghệ tương lai Công cụ và hướng dẫn
Ethics Là Gì? Bách Khoa Toàn Thư Về Đạo Đức Học, Ý Nghĩa Và Ứng Dụng Thực Tiễn Trong Kỷ Nguyên Hiện Đại
Trong một thế giới mà ranh giới giữa đúng và sai ngày càng trở
Xem thêmTh4
Digital Maketing Kiến thức SEO Marketing/ bán hàng/ Doanh nghiệp
Curate là gì? Cẩm Nang Toàn Tập Về Nghệ Thuật Giám Tuyển Nội Dung (Content Curation) Từ A-Z
Trong kỷ nguyên bùng nổ thông tin hiện nay, mỗi ngày có hàng triệu
Xem thêmTh4