Nếu bạn đã từng dạo quanh các diễn đàn lập trình hay tham gia các buổi phỏng vấn về JavaScript, chắc chắn bạn đã nghe đến từ khóa "Closure". Nó thường được miêu tả như một khái niệm khó nhằn, trừu tượng, nhưng cũng là chìa khóa để mở ra cánh cửa của một lập trình viên JavaScript chuyên nghiệp.

Vậy Closure thực sự là gì? Tại sao nó lại quan trọng đến vậy? Và làm thế nào để chúng ta không chỉ hiểu mà còn có thể sử dụng nó một cách thành thạo? Hãy cùng nhau vén bức màn bí ẩn này nhé!
1. Closure là gì? Một định nghĩa dễ hiểu 💡
Hãy quên đi những định nghĩa học thuật phức tạp trong giây lát. Hãy tưởng tượng thế này:
Một hàm (function) giống như một người công nhân. Khi được tạo ra, người công nhân này được phát cho một chiếc ba lô. Bên trong chiếc ba lô đó chứa tất cả những "dụng cụ" (biến, hằng số) mà người đó có thể cần, lấy từ chính nơi anh ta được "sinh ra". Ngay cả khi người công nhân này được cử đi làm ở một nơi hoàn toàn khác, anh ta vẫn luôn mang theo chiếc ba lô đó và có thể sử dụng các dụng cụ bên trong.
Closure chính là sự kết hợp của hàm đó và chiếc ba lô (phạm vi từ vựng) mà nó mang theo.

Bây giờ, hãy đến với định nghĩa chính thức hơn:
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. Closure cho phép một hàm con truy cập và thao tác với các biến của hàm cha, ngay cả khi hàm cha đã thực thi xong.
Nghe vẫn hơi khó hiểu? Đừng lo, hãy xem ví dụ kinh điển sau đây.
function createCharacter() {
let characterName = 'Luffy' // Biến được định nghĩa trong hàm cha
function showName() {
// Hàm con
console.log(characterName) // Có thể truy cập biến của hàm cha
}
return showName // Trả về chính hàm con
}
// Gọi hàm createCharacter, nó thực thi và trả về hàm showName
// Chúng ta gán hàm được trả về vào biến callName
const callName = createCharacter()
// Bây giờ, hàm createCharacter đã chạy xong.
// Về lý thuyết, biến 'characterName' phải bị xóa khỏi bộ nhớ.
// NHƯNG...
callName() // Output: "Luffy"
Điều kỳ diệu gì đã xảy ra?
Khi createCharacter được gọi, nó đã trả về hàm showName. Hàm showName này khi được tạo ra đã "đóng gói" (closed over) môi trường của nó, bao gồm cả biến characterName. Nó mang theo "chiếc ba lô" chứa characterName bên mình. Vì vậy, dù createCharacter đã kết thúc, callName (chính là showName) vẫn nhớ và truy cập được characterName. Đó chính là Closure!
2. Tại sao Closures lại quan trọng? Sức mạnh thực sự 🧠
Closures không chỉ là một câu đố lý thuyết. Chúng là nền tảng cho rất nhiều mẫu thiết kế (design patterns) và tính năng mạnh mẽ trong JavaScript.
🔐 Data Encapsulation - Che giấu dữ liệu và tạo biến riêng tư
Trong nhiều ngôn ngữ lập trình hướng đối tượng, bạn có các từ khóa như private để bảo vệ dữ liệu. JavaScript (trước đây) không có khái niệm này một cách chính thức, và Closures chính là cứu cánh.
Hãy xem ví dụ về một bộ đếm (counter):
function createCounter() {
let count = 0 // Biến 'count' này là "riêng tư"
return {
increment: function () {
count++
console.log(count)
},
decrement: function () {
count--
console.log(count)
},
getValue: function () {
return count
},
}
}
const counter = createCounter()
counter.increment() // Output: 1
counter.increment() // Output: 2
counter.decrement() // Output: 1
// Bạn không thể truy cập trực tiếp vào 'count' từ bên ngoài
console.log(counter.count) // Output: undefined
Ở đây, biến count sống bên trong createCounter. Chúng ta không thể truy cập hay thay đổi nó từ bên ngoài. Cách duy nhất để tương tác với count là thông qua các phương thức increment, decrement, và getValue được trả về. Các phương thức này tạo thành một closure, "nhớ" và chia sẻ cùng một biến count. Đây chính là nguyên tắc che giấu dữ liệu.
🏭 Function Factories - Nhà máy sản xuất hàm
Closures cho phép bạn tạo ra các hàm đã được "cấu hình sẵn".
function makeGreeter(greeting) {
return function (name) {
console.log(`${greeting}, ${name}!`)
}
}
const sayHello = makeGreeter('Hello')
const sayXinChao = makeGreeter('Xin chào')
sayHello('John') // Output: Hello, John!
sayXinChao('Sơn') // Output: Xin chào, Sơn!
Hàm makeGreeter là một "nhà máy". Mỗi lần gọi nó, bạn tạo ra một hàm mới (sayHello, sayXinChao) đã được "ghi nhớ" sẵn giá trị greeting khác nhau.
⏳ Callbacks và Asynchronous programming
Đây là một trong những ứng dụng phổ biến nhất của closures. Khi bạn làm việc với setTimeout, event listeners, hay Promises, bạn đang sử dụng closures mọi lúc mà có thể không nhận ra.
function waitAndSay(message, delay) {
setTimeout(function () {
// Hàm callback này là một closure
// Nó "nhớ" biến 'message' từ môi trường bên ngoài
console.log(message)
}, delay)
}
waitAndSay('Đợi 3 giây và tôi xuất hiện.', 3000)
Hàm callback được truyền vào setTimeout sẽ được thực thi sau 3 giây. Tại thời điểm đó, hàm waitAndSay đã chạy xong từ lâu. Nhưng nhờ có closure, hàm callback vẫn "nhớ" được giá trị của message khi nó được tạo ra.
3. Cạm bẫy thường gặp: Vòng lặp và Closures ⚠️
Đây là một ví dụ kinh điển thường xuất hiện trong các buổi phỏng vấn, làm bối rối rất nhiều lập trình viên.
for (var i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i)
}, i * 1000)
}
Nhiều người sẽ kỳ vọng kết quả là 1, 2, 3, 4, 5 được in ra sau mỗi giây. Nhưng kết quả thực tế lại là:
6
6
6
6
6
Tại sao?
varcó phạm vi hàm (function scope): Chỉ có một biếniduy nhất được chia sẻ cho tất cả các lần lặp.- Bất đồng bộ: Vòng lặp
forchạy rất nhanh và hoàn thành gần như ngay lập tức. Nó xếp 5 lệnhsetTimeoutvào hàng đợi. Khi vòng lặp kết thúc, giá trị củailà6. - Closure: Sau 1 giây, 2 giây,... khi các hàm callback trong
setTimeoutđược thực thi, chúng truy cập vào biếni. Vì tất cả chúng đều tham chiếu đến cùng một biếni, chúng đều thấy giá trị cuối cùng của nó là6.
Cách giải quyết?
✅ Sử dụng let: Đây là cách hiện đại và đơn giản nhất. let có phạm vi khối (block scope), nghĩa là mỗi lần lặp của vòng for, một biến i mới sẽ được tạo ra.
for (let i = 1; i <= 5; i++) {
setTimeout(function () {
// Mỗi callback bây giờ có một closure với biến 'i' riêng của nó
console.log(i)
}, i * 1000)
}
// Kết quả: 1, 2, 3, 4, 5 (đúng như mong đợi)
Kết luận: Closures không phải là "ma thuật" 🔮
Closures không phải là thứ gì đó quá khó hiểu. Đó là một hệ quả tự nhiên của cách JavaScript xử lý phạm vi biến (cụ thể là Lexical Scoping - phạm vi được xác định tại thời điểm viết code, không phải lúc chạy code).
Việc nắm vững Closures sẽ giúp bạn:
- Viết code sạch hơn, module hóa tốt hơn.
- Hiểu sâu hơn về các khái niệm cốt lõi như scope, context.
- Tự tin chinh phục các mẫu thiết kế nâng cao và các framework JavaScript hiện đại.
Hy vọng rằng qua bài viết này, "Closure" không còn là một từ khóa đáng sợ, mà đã trở thành một công cụ mạnh mẽ trong bộ kỹ năng JavaScript của bạn. Hãy thực hành, thử nghiệm với các ví dụ, và bạn sẽ thấy sức mạnh của nó thật đáng kinh ngạc.
Chúc bạn thành công!
![[Advanced JS] IIFE là gì? Ứng dụng nâng cao của IIFE trong JavaScript](/images/blog/iife-in-javascript.webp)
![[Advanced JS] Event Loop là gì? Khám phá cơ chế hoạt động của JavaScript](/images/blog/event-loop-and-how-javascript-works.webp)
![[Advanced JS] Cấu trúc dữ liệu & Thuật toán: Ứng dụng thực tế với JavaScript](/images/blog/data-structures-and-algorithms-with-javascript.webp)
![[JS Basics] Phân biệt Shallow Copy và Deep Copy trong JavaScript](/images/blog/shallow-copy-and-deep-copy-in-javascript.webp)