Closure trong JavaScript là gì? Giải thích bằng ví dụ dễ hiểu

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Closure là khái niệm thường xuất hiện trong phỏng vấn JavaScript và khiến nhiều người mới thấy mơ hồ. Thực ra bạn đã dùng closure hằng ngày mà không biết: mỗi khi viết một callback dùng biến bên ngoài, đó là closure đang làm việc. Bài này giải thích bằng ví dụ thay vì định nghĩa khô khan.

Định nghĩa ngắn gọn

Closure là một hàm cùng với môi trường mà nó được tạo ra. Nói cách khác, một hàm "nhớ" các biến ở phạm vi bên ngoài nơi nó được định nghĩa, ngay cả khi hàm bên ngoài đã chạy xong. Cách hình dung quen thuộc: hàm mang theo một chiếc ba lô chứa những biến nó cần.

Ví dụ đầu tiên: bộ đếm

function taoBoDem() {
  let dem = 0;
  return function () {
    dem++;
    return dem;
  };
}

const boDem = taoBoDem();
console.log(boDem()); // 1
console.log(boDem()); // 2
console.log(boDem()); // 3

const boDem2 = taoBoDem();
console.log(boDem2()); // 1, độc lập với boDem

Khi taoBoDem() chạy xong, thông thường biến dem sẽ biến mất. Nhưng hàm bên trong vẫn tham chiếu đến nó, nên JavaScript giữ biến đó lại. Mỗi lần gọi taoBoDem tạo ra một biến dem riêng, vì thế hai bộ đếm không ảnh hưởng nhau.

Dùng closure để tạo dữ liệu riêng tư

JavaScript trước đây không có thuộc tính private thực sự, closure là cách phổ biến để giấu dữ liệu. Ví dụ ví tiền nhỏ chỉ cho phép thao tác qua các hàm:

function taoVi(soDuBanDau) {
  let soDu = soDuBanDau;
  return {
    nap(tien) { soDu += tien; },
    rut(tien) {
      if (tien > soDu) throw new Error('Không đủ tiền');
      soDu -= tien;
    },
    xem() { return soDu; },
  };
}

const vi = taoVi(100);
vi.nap(50);
console.log(vi.xem()); // 150
console.log(vi.soDu);  // undefined, không truy cập trực tiếp được

Bên ngoài không thể sửa soDu trực tiếp, chỉ qua ba phương thức. Ngày nay JavaScript đã có trường private của class với tiền tố #, nhưng closure vẫn là nền tảng bạn cần hiểu.

Hàm tạo hàm

Closure cho phép sản xuất các hàm chuyên biệt từ một khuôn:

function nhanVoi(heSo) {
  return (x) => x * heSo;
}
const gapDoi = nhanVoi(2);
const gapBa = nhanVoi(3);
console.log(gapDoi(10)); // 20
console.log(gapBa(10));  // 30

Mỗi hàm trả về nhớ giá trị heSo của riêng nó. Kỹ thuật này gọi là currying hoặc partial application ở dạng đơn giản, và là cơ sở của nhiều mẫu thiết kế, từ debounce đến middleware.

Closure và vòng lặp: lỗi kinh điển

Đoạn code sau thường gây bất ngờ:

for (var i = 1; i <= 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 100);
}
// in ra: 4 4 4

Vì var có phạm vi theo hàm, cả ba callback chia sẻ cùng một biến i, và khi chúng chạy thì vòng lặp đã kết thúc với i bằng 4. Sửa bằng let, vì let tạo biến mới cho mỗi vòng lặp:

for (let i = 1; i <= 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 100);
}
// in ra: 1 2 3

Ví dụ này cho thấy closure giữ tham chiếu đến biến, không phải bản sao của giá trị tại thời điểm tạo hàm.

Closure trong công việc hằng ngày

  • Callback sự kiện: trình xử lý click dùng biến của hàm bao ngoài.
  • Debounce và throttle: dùng biến timer được giữ qua nhiều lần gọi.
  • Hook của React: useState và useEffect hoạt động dựa trên closure, nên bạn gặp lỗi "giá trị cũ" (stale closure) khi hàm giữ lại biến của lần render trước.
  • Module: mẫu module bọc dữ liệu riêng trong một hàm tự gọi.

Lưu ý về bộ nhớ

Vì closure giữ các biến sống lâu, nếu chúng tham chiếu đến đối tượng lớn (như phần tử DOM hay mảng khổng lồ) mà bạn không còn cần, bộ nhớ không được giải phóng. Hãy gỡ trình lắng nghe sự kiện khi không dùng nữa và tránh giữ tham chiếu thừa. Phần lớn trường hợp thông thường thì không cần lo, đừng tối ưu sớm.

Bài tập tự kiểm tra

Hãy đoán kết quả của đoạn mã dưới đây trước khi chạy:

function tao() {
  let n = 10;
  return {
    tang: () => ++n,
    xem: () => n,
  };
}
const a = tao();
a.tang();
a.tang();
console.log(a.xem());   // ?
console.log(tao().xem()); // ?

Đáp án là 12 và 10. Hai hàm tang và xem trong cùng một đối tượng chia sẻ chung một biến n, còn mỗi lần gọi tao() lại sinh ra một biến n mới. Nếu bạn trả lời đúng cả hai, bạn đã nắm được ý chính của closure rồi.

Kết luận

Closure đơn giản là hàm nhớ môi trường nơi nó được tạo ra. Hãy tự gõ lại ví dụ bộ đếm và ví dụ vòng lặp, rồi thử biến tấu. Khi tự đoán được kết quả trước khi chạy, bạn đã thật sự hiểu closure.

Nếu bạn muốn ôn JavaScript kỹ càng để tự tin hơn khi đi phỏng vấn, hãy tham khảo các khóa học trên codebem.com.

author_photo
Nguyễn Đình Cường

0 comment