JavaScript Closures Là Gì? Hiểu Rõ Cách Hàm Ghi Nhớ Biến Và Ứng Dụng Thực Tế
Jason Nguyen
Ngày công bố: 22/11/2025
Tác giả
Ngày công bố: 22/11/2025
JavaScript Closures Là Gì? Hiểu Rõ Cách Hàm Ghi Nhớ Biến Và Ứng Dụng Thực Tế
Closure (bao đóng) là một trong những khái niệm quan trọng và cũng gây nhiều bối rối nhất đối với lập trình viên JavaScript. Dù bạn là người mới bắt đầu hay đã có kinh nghiệm, việc hiểu rõ closures sẽ giúp bạn viết code sạch hơn, tận dụng được sức mạnh của lập trình hàm, và giải quyết nhiều vấn đề thực tế một cách hiệu quả. Bài viết này sẽ giải thích closures từ góc độ đơn giản nhất, đi sâu vào cách hoạt động, và đưa ra các ví dụ ứng dụng thực tế trong công việc hàng ngày của lập trình viên.
1. Closure là gì? Định nghĩa đơn giản và dễ hiểu
Một closure là một hàm có thể ghi nhớ và truy cập vào các biến ở phạm vi bên ngoài (lexical scope) của nó, ngay cả khi hàm đó được thực thi ở một phạm vi khác. Nói cách khác, closure là sự kết hợp giữa một hàm và môi trường (environment) mà nó được tạo ra. Môi trường này bao gồm tất cả các biến cục bộ có sẵn tại thời điểm closure được định nghĩa.
Hãy xem ví dụ đơn giản nhất:
function outer() {
let message = "Hello từ bên ngoài!";
function inner() {
console.log(message); // Truy cập biến của outer
}
return inner;
}
const myClosure = outer();
myClosure(); // In ra: "Hello từ bên ngoài!"
Trong ví dụ này, hàm inner đã ghi nhớ biến message của outer ngay cả khi outer đã thực thi xong. Đó chính là closure.
2. Cơ chế hoạt động: Lexical Scoping và bộ nhớ của hàm
Để hiểu closures, trước hết bạn cần nắm vững lexical scoping (phạm vi từ vựng). Trong JavaScript, phạm vi của một biến được xác định bởi vị trí của nó trong mã nguồn (nơi nó được khai báo). Hàm con có thể truy cập vào biến của hàm cha, nhưng ngược lại thì không.
function parent() {
let name = "Alice";
function child() {
let age = 25;
console.log(name); // "Alice" – truy cập được
}
console.log(age); // ReferenceError: age is not defined
}
Khi một hàm được định nghĩa, nó sẽ "nhớ" phạm vi từ vựng của nó – tức là tất cả các biến có sẵn tại thời điểm định nghĩa. Khi hàm đó được gọi sau này (thậm chí ở một vị trí khác trong code), nó vẫn có quyền truy cập vào phạm vi cũ đó. Chính điều này tạo nên closure.
3. Ứng dụng thực tế của Closure
3.1. Tạo biến Private – Module Pattern
Trước khi có class (ES6) và từ khóa #, closures là cách duy nhất để tạo biến private trong JavaScript. Đây là ứng dụng phổ biến nhất và quan trọng nhất.
function createCounter() {
let count = 0; // Biến private
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.getCount()); // 0
counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined – không thể truy cập từ bên ngoài
Trong ví dụ này, biến count nằm trong closure và chỉ có các hàm increment, decrement, getCount mới có quyền truy cập. Đây là nền tảng của Module Pattern – một pattern quan trọng trong JavaScript.
3.2. Event Handlers và Callbacks
Closures thường xuất hiện trong các event handler và callback. Mỗi khi bạn gán một hàm xử lý sự kiện, hàm đó "ghi nhớ" các biến xung quanh nó tại thời điểm định nghĩa.
function setupButton(buttonId) {
let clickCount = 0;
const button = document.getElementById(buttonId);
button.addEventListener('click', function() {
clickCount++;
console.log(`Button clicked ${clickCount} times`);
});
}
setupButton('myButton');
Mỗi lần button được click, closure của event handler vẫn nhớ và cập nhật biến clickCount.
3.3. Function Factories (Hàm tạo ra hàm)
Closures cho phép bạn tạo ra các hàm chuyên biệt từ một hàm tổng quát.
function createMultiplier(multiplier) {
return function(value) {
return value * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
3.4. Memoization – Cache kết quả để tăng hiệu suất
Memoization là một kỹ thuật tối ưu hóa bằng cách lưu trữ kết quả của các hàm tốn kém tài nguyên và trả về kết quả đã lưu khi đầu vào không đổi. Closure giúp lưu trữ cache một cách an toàn.
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key] !== undefined) {
console.log('From cache');
return cache[key];
}
console.log('Computing...');
const result = fn(...args);
cache[key] = result;
return result;
};
}
const expensiveFn = (a, b) => a + b * a;
const memoized = memoize(expensiveFn);
console.log(memoized(2, 3)); // Computing... 8
console.log(memoized(2, 3)); // From cache 8
3.5. Tạo Partial Application và Currying
Currying là kỹ thuật biến một hàm nhận nhiều tham số thành một chuỗi các hàm mỗi hàm nhận một tham số. Closure là công cụ để implement currying.
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
};
}
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
4. Closure và vòng lặp – Cạm bẫy kinh điển và cách khắc phục
Đây là một trong những lỗi phổ biến nhất khi làm việc với closures và vòng lặp.
// Vấn đề: Tất cả đều in ra 5
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
// In ra: 5, 5, 5, 5, 5
Tại sao? Vì biến i được khai báo với var có phạm vi hàm, tất cả các closure trong vòng lặp đều tham chiếu đến cùng một biến i. Khi setTimeout chạy, vòng lặp đã kết thúc và i đã là 5.
Giải pháp 1: Sử dụng let thay vì var
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
// In ra: 0, 1, 2, 3, 4
let có phạm vi block, mỗi vòng lặp tạo ra một biến i riêng biệt, do đó mỗi closure ghi nhớ một giá trị khác nhau.
Giải pháp 2: IIFE (Immediately Invoked Function Expression)
for (var i = 0; i < 5; i++) {
(function(index) {
setTimeout(function() {
console.log(index);
}, 100);
})(i);
}
// In ra: 0, 1, 2, 3, 4
Mỗi vòng lặp, IIFE nhận giá trị hiện tại của i và lưu nó vào một closure mới với biến index.
5. Closure và quản lý bộ nhớ
Một lưu ý quan trọng: closures giữ tham chiếu đến phạm vi bên ngoài, điều này có thể ngăn garbage collection thu hồi bộ nhớ, dẫn đến rò rỉ bộ nhớ (memory leak) nếu không cẩn thận.
// Rò rỉ bộ nhớ tiềm năng
function createHeavyObject() {
const heavyData = new Array(1000000).fill('big data');
return function() {
console.log(heavyData.length);
};
}
const closure = createHeavyObject();
// heavyData vẫn được giữ trong bộ nhớ
// Khi không dùng, giải phóng tham chiếu
closure = null; // Cho phép garbage collection
6. Closure trong React Hooks
React Hooks, đặc biệt là useState và useEffect, dựa trên closures để hoạt động. Mỗi lần render, hooks tạo ra một closure mới với state và props của lần render đó.
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
// Closure này "nhớ" giá trị count tại thời điểm effect được tạo
const timer = setInterval(() => {
console.log(count); // In ra giá trị count tại thời điểm setInterval được gọi
}, 1000);
return () => clearInterval(timer);
}, []); // Dependency array rỗng – chỉ chạy một lần
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
Trong ví dụ này, useEffect tạo một closure ghi nhớ giá trị count tại lần render đầu tiên. Vì dependency array rỗng, effect không chạy lại, do đó count trong closure luôn là 0. Để khắc phục, bạn cần thêm count vào dependency array hoặc dùng useRef.
7. Tổng kết – Closure là một công cụ không thể thiếu
Closures không chỉ là một khái niệm lý thuyết khó hiểu, mà là một công cụ thực tiễn cực kỳ quan trọng trong JavaScript. Chúng xuất hiện ở mọi nơi: từ các thư viện hàm, xử lý bất đồng bộ, đến các framework và design patterns. Một khi bạn đã thực sự hiểu closures, bạn sẽ:
- Viết code rõ ràng hơn, dễ bảo trì hơn.
- Khắc phục được các lỗi liên quan đến vòng lặp và asynchronous.
- Tạo ra các module và hàm mạnh mẽ với private state.
- Hiểu sâu hơn về cách React Hooks hoạt động.
Hãy thực hành nhiều với các ví dụ trên và tự tạo ra các tình huống của riêng mình để nhuần nhuyễn kỹ năng này. Chúc bạn thành công!
Bình luận (0)
No comments yet. Be the first to share your thoughts!