Skip to content

Lập trình/ Code

Closure Là Gì? Giải Mã Khái Niệm “Hack Não” Nhất Trong JavaScript (Hướng Dẫn Toàn Tập 2025)

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 […]

Văn Tân
Văn Tân Tác giả
14 phút đọc 2,641 từ 269 lượt xem
Closure Là Gì? Giải Mã Khái Niệm “Hack Não” Nhất Trong JavaScript (Hướng Dẫn Toàn Tập 2025)
Mục lục bài viết (14 mục)
Có thể bạn biết?

Closure là gì? Tại sao nó khiến 90% Newbie bối rối? Bài viết giải thích tường tận về Lexical Scope, cơ chế bộ nhớ và ví dụ thực tế dễ hiểu nhất. Xem ngay!

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ố đó.

Closure là gì
Closure là gì

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 Closure là gì
Định nghĩa Closure là gì

Đị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:

  1. Hàm con nhìn thấy biến của chính nó.

  2. Hàm con nhìn thấy biến của hàm cha.

  3. 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.

Cơ chế hoạt động của Closure
Cơ chế hoạt động của Closure

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:

JavaScript
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: 700

Giả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ế.

Ứng dụng thực tế của Closure trong Lập trình
Ứng dụng thực tế của Closure trong Lập trình

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)); // 15

Kỹ 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... 15

Cạ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.

Closure trong vòng lặp
Closure trong vòng lặp

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, 3

Cá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.

Closure và Memory Leak
Closure và Memory Leak

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ị null cho 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!

Closure trong Modern JavaScript
Closure trong Modern JavaScript

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:

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:

  1. Nắm vững Lexical Scope.

  2. Viết code tạo private variable bằng Closure.

  3. Thử giải thích lỗi vòng lặp for và var cho người khác hiểu.

  4. 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

Chia sẻ:
Văn Tân
Về Tác Giả ✓ Verified

Văn Tân

Chào bạn, mình là Văn Tân hay còn gọi là Tân Digi - Người sáng lập ra TaSDigital, Tân rất đam mê về công nghệ và thích viết…

Tất cả bài viết

Bình luận (0)

Viết bình luận của bạn

Đánh dấu * là bắt buộc

Báo cáo vi phạm

Báo cáo nội dung

Nếu bạn phát hiện nội dung sai lệch, vi phạm bản quyền hoặc cần cập nhật, vui lòng gửi thông báo cho chúng tôi.

Trợ lý AI TAS Digital
Câu hỏi không được lưu trữ — Chính sách bảo mật

Trợ lý AI TAS Digital — trả lời tức thì 24/7