1. Closure là gì ?
Closure thường được dịch là "hàm đóng", "hàm khép kín" hoặc đơn giản là "hàm có vùng nhớ riêng". Tuy nhiên, để dễ hiểu hơn Closure là một cơ chế giúp một hàm con “ghi nhớ” được các biến ở phạm vi cha của nó, ngay cả khi hàm cha đã kết thúc. Vậy tại sao phải dùng closure để làm điều đó ? Vì bình thường, khi một hàm kết thúc, biến bên trong nó sẽ bị "giải phóng khỏi bộ nhớ" (GC - Garbage Collection) Nhưng nếu có một closure, tức là có một hàm con vẫn đang sử dụng biến của hàm cha, thì những biến đó sẽ được giữ lại trong bộ nhớ. Việc biến đó vẫn được giữ lại trong bộ nhớ ngay cả khi hàm cha đã kết thúc rồi sẽ là công cụ rất mạnh để xây dựng các chức năng nhớ được “lần trước” như:
- Tạo bộ đếm
- cache
- debounce
- throttle
- Tối ưu hiệu suất: Cache kết quả để khỏi tính toán lại
Ví dụ minh họa
function createCounter() { let count = 0; return function () { count++; return count; };
} const counter = createCounter(); console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
Giải thích:
- createCounter() là hàm cha, trả về một hàm con.
- Biến count nằm trong phạm vi của hàm cha createCounter.
- Sau khi createCounter() chạy xong, lẽ ra count phải biến mất khỏi bộ nhớ.
- Nhưng! vì hàm con đang "đóng gói" (closure) và sử dụng count, nên count vẫn tồn tại và giữ giá trị mỗi lần gọi tiếp theo.
Vậy đến đây có thể kết luận 1 tính năng của Closure là “nhớ trạng thái” (state) qua thời gian, nhưng không làm lộ biến ra ngoài. Closure cho phép tạo ra một vùng nhớ riêng biệt, nơi có thể lưu thông tin mà chỉ có hàm con mới truy cập được, người bên ngoài không can thiệp được.
function createSecretHolder(secret) { return { getSecret: function() { return secret; }, setSecret: function(newSecret) { secret = newSecret; } };
} const holder = createSecretHolder('abc123');
console.log(holder.getSecret()); // 'abc123'
holder.setSecret('xyz789');
console.log(holder.getSecret()); // 'xyz789'
→ Ở đây, biến secret được giữ lại bởi closure và không ai bên ngoài có thể truy cập trực tiếp, chỉ có thể thông qua hàm getSecret() và setSecret() → giống như private variable trong OOP.
2. Một số ứng dụng trong các bài toán thực tế
Có vẻ vẫn khá khó hiểu đúng không, đến đây chúng ta sẽ đi vào 1 số ví dụ thực tế qua các bài toán khi code để rõ hơn nhé.
Ví dụ 1: Tạo một custom hook có thể đếm số lần người dùng click nhưng không cập nhật lại component mỗi lần (tức là state “ẩn”, không render lại).
getCounter trả về một hàm có closure ghi nhớ biến countRef, Mỗi lần click, countRef.current tăng lên, không cần re-render, Closure giúp duy trì trạng thái click mà không phụ thuộc vào React state. Điều này tránh việc re-render không cần thiết, đúng nguyên tắc sử dụng state trong react. Chúng ta dùng qua ref bản chất ở đây chính là closure.
import { useEffect, useRef } from "react"; export function useClickCounter() { const countRef = useRef(0); // không trigger re-render function getCounter() { // Đây chính là closure return function () { countRef.current += 1; console.log("Số lần click:", countRef.current); }; } return getCounter();
} // Sử dụng
import React from "react";
import { useClickCounter } from "./useClickCounter"; export default function App() { const handleClick = useClickCounter(); return ( <button onClick={handleClick}> Click me! </button> );
}
Ví dụ 2 Debounce function trong ô tìm kiếm
Vấn đề gặp phải: Khi người dùng gõ vào ô tìm kiếm (input), ta không muốn gọi API liên tục theo từng phím gõ, vì:
- Gây tốn tài nguyên
- Server dễ bị quá tải
- UX không mượt (gọi API liên tục → lag, delay)
Giải pháp: Dùng closure để debounce (chờ yên một lúc mới gọi API). Các bước như sau:
- Người dùng gõ: "h", "he", "hel", "hell", "hello"
- Mỗi lần debouncedSearch() được gọi, closure giữ lại timerId trước đó
- Nếu chưa đủ 500ms mà người dùng lại gõ tiếp → timeout cũ bị huỷ
- Chỉ khi dừng gõ 500ms → callback gọi API mới được thực thi
Closure giúp ghi nhớ timeoutId của setTimeout, để lần sau có thể huỷ lần gọi cũ, tránh gọi API thừa.
function debounce(callback, delay) { let timerId; // closure giữ lại giữa các lần gọi return function (...args) { clearTimeout(timerId); // huỷ lần trước timerId = setTimeout(() => { callback(...args); // gọi callback sau delay }, delay); };
} // Sử dụng
import React from "react"; function App() { // Callback gọi API const fetchSearchResult = (query) => { console.log("Call API with:", query); // giả lập gọi API }; // Tạo hàm debounce const debouncedSearch = React.useMemo(() => debounce(fetchSearchResult, 500), []); return ( <input type="text" placeholder="Search..." onChange={(e) => debouncedSearch(e.target.value)} /> );
}
3. So sánh Closure với Class và tính bao đóng trong OOP (encapsulation)
Lướt qua có vẻ giống giống với tính bao đóng, 1 trong 4 tính chất của OOP mà mình đã từng học ở môn lập trình OOP ở Đại học đúng không ? Closure có vẻ đã mô phỏng lại tính bao đóng (encapsulation) một cách tự nhiên trong JavaScript — vốn là ngôn ngữ hướng hàm (functional), không thuần hướng đối tượng. Closure giống như OOP nghèo — không có từ khóa private, class, nhưng vẫn giữ được nguyên tắc ẩn thông tin và kiểm soát truy cập.
Nếu bạn đang học JS, thì closure chính là cách giúp bạn áp dụng tư duy OOP một cách đơn giản mà mạnh mẽ.
Cảm ơn anh em đã đọc bài. Bài viết có sự hỗ trợ của GPT để diễn đạt câu cú cho chuẩn và đỡ phải gõ nhiều 😆😆😆