Closure Trong JavaScript: Hiểu Tường Tận Để Lập Trình Như Chuyên Gia

Closure — hay còn gọi là "hàm đóng" — là một trong những khái niệm quyền năng và cũng dễ gây bối rối nhất trong JavaScript. Nắm vững Closure không chỉ giúp bạn vượt qua phỏng vấn kỹ thuật cấp Senior mà còn thay đổi cách bạn kiến trúc toàn bộ codebase: gọn hơn, an toàn hơn, và ít bug hơn. Nhiều người hay nhầm lẫn giữa Scope và Closure, nhưng thực ra chúng có quan hệ chặt chẽ và bổ trợ cho nhau. Hãy cùng khám phá từ nền tảng.

closureJavaScriptbiếnphạm viscopehàmfunction
Ảnh bìa bài viết: Closure Trong JavaScript: Hiểu Tường Tận Để Lập Trình Như Chuyên Gia
Ảnh đại diện của Trung Vũ Hoàng

Trung Vũ Hoàng

Tác giả

29/3/20265 phút đọc

1. Closure Là Gì?

Về bản chất, một Closure là sự kết hợp giữa một hàmmôi trường từ vựng nơi hàm đó được định nghĩa. Trong JavaScript, mọi hàm đều là closure vì chúng ghi nhớ phạm vi (scope) nơi chúng được tạo ra — kể cả sau khi hàm bên ngoài đã kết thúc thực thi.

Hãy hình dung thế này: bạn có một chiếc hộp đặc biệt. Khi bạn lấy một món đồ từ trong hộp ra và đóng nắp hộp lại, món đồ đó vẫn "ghi nhớ" môi trường bên trong chiếc hộp — như thể nó mang theo một phần của chiếc hộp đi cùng. Closure hoạt động chính xác theo cơ chế đó.

Nền tảng cho điều này là Lexical Scoping (Phạm vi từ vựng): JavaScript xác định phạm vi của biến dựa theo vị trí của nó trong mã nguồn lúc viết, không phải lúc chạy.

2. Cơ Chế Hoạt Động Bên Trong

Khi một hàm được gọi, JavaScript tạo ra một Execution Context (ngữ cảnh thực thi) để lưu các biến cục bộ. Thông thường, khi hàm kết thúc, ngữ cảnh này bị trình thu gom rác (Garbage Collector) dọn sạch.

Nhưng nếu có một hàm con bên trong vẫn đang tham chiếu đến các biến của hàm cha, JavaScript sẽ giữ nguyên môi trường đó trong bộ nhớ. Hàm con "đóng gói" (close over) những biến xung quanh nó, ngăn không cho chúng biến mất. Đây là lý do kỹ thuật này mang tên Closure.

Cảnh báo: Lạm dụng Closure mà không hiểu cơ chế dọn bộ nhớ có thể dẫn đến Memory Leak — một loại lỗi âm thầm khiến ứng dụng ăn RAM ngày càng nhiều mà không báo lỗi rõ ràng.

3. Ví Dụ Từ Cơ Bản Đến Nâng Cao

Cách dễ nhất để "nhìn thấy" Closure là qua một bộ đếm đơn giản:

function createCounter() {
  let count = 0; // Biến này thuộc phạm vi hàm cha

  return function() {
    count++;       // Hàm con truy cập và thay đổi biến của cha
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

Biến count không thể bị truy cập trực tiếp từ bên ngoài — nó bị "nhốt" bên trong Closure. Nhưng hàm được trả về vẫn có thể đọc và thay đổi nó. Mỗi lần gọi counter(), giá trị tiếp tục từ lần trước — đây là trạng thái (state) bền vững không cần class.

Một điểm thú vị: nếu bạn tạo counter2 = createCounter(), nó sẽ có vùng nhớ hoàn toàn độc lập với counter. Mỗi Closure sở hữu riêng bản sao dữ liệu của mình.

4. Ứng Dụng Thực Tế Trong Lập Trình Chuyên Nghiệp

Closure không phải khái niệm hàn lâm — nó xuất hiện ở khắp mọi nơi trong React, Vue và các design pattern phổ biến:

Tạo Private Variables

JavaScript truyền thống không có từ khóa private. Closure là cách tự nhiên nhất để tạo biến riêng tư — che giấu trạng thái nội bộ, chỉ lộ ra những phương thức cần thiết.

Module Pattern

Bọc toàn bộ logic trong một IIFE và trả về API công khai. Giúp tránh ô nhiễm namespace toàn cục — cực kỳ quan trọng trong dự án nhiều developer cùng làm việc.

Currying

Biến một hàm nhận nhiều tham số thành chuỗi các hàm nhận một tham số, mỗi hàm ghi nhớ tham số trước thông qua Closure.

Function Factories

Tạo ra các hàm có cùng cấu trúc nhưng dữ liệu khởi tạo khác nhau — như tạo trình xử lý sự kiện cho nhiều nút bấm cùng lúc.

Xử lý bất đồng bộ

Lưu giữ đúng giá trị biến trong vòng lặp khi làm việc với setTimeout, Promise, hoặc gọi API có delay.

5. Ưu Điểm Và Nhược Điểm

Ưu điểm

  • Đóng gói dữ liệu tốt (Encapsulation) — bảo vệ trạng thái nội bộ.

  • Giảm thiểu biến toàn cục, tránh xung đột logic.

  • Duy trì trạng thái giữa các lần gọi hàm mà không cần class.

Nhược điểm

  • Tiêu tốn bộ nhớ hơn — biến không được giải phóng ngay khi hàm kết thúc.

  • Lồng ghép nhiều lớp closure khiến code khó debug.

  • Có thể ảnh hưởng hiệu suất nếu tạo hàng nghìn closure trong vòng lặp cường độ cao.

Mẹo tối ưu bộ nhớ: Khi không còn cần dùng Closure nữa, gán biến chứa hàm đó bằng null để Garbage Collector có thể dọn dẹp vùng nhớ liên quan.

6. Sai Lầm Kinh Điển Cần Tránh

Một lỗi mà hầu như ai mới học JS cũng mắc: dùng Closure trong vòng lặp for với từ khóa var:

// Kết quả sai — in ra 4, 4, 4 thay vì 1, 2, 3
for (var i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

// Kết quả đúng — dùng let tạo scope riêng cho từng vòng lặp
for (let i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i); // In ra 1, 2, 3
  }, 1000);
}

Lý do: var bị hoisting lên phạm vi hàm — tất cả callback dùng chung một biến i. Khi timeout thực thi, vòng lặp đã kết thúc và i = 4. Từ khóa let tạo phạm vi block riêng cho mỗi vòng lặp, giải quyết triệt để vấn đề này.

Tổng Kết

Closure là nền tảng của rất nhiều pattern quan trọng trong JavaScript hiện đại: từ React Hooks (bản thân useState dựa trên Closure), đến Module Pattern, Currying, và Memoization. Hiểu sâu Closure giúp bạn:

  • Viết code bảo mật và đóng gói tốt hơn.

  • Tránh được các bug tinh vi liên quan đến scope và bộ nhớ.

  • Đọc và hiểu source code của các thư viện nổi tiếng.

Bước tiếp theo để củng cố kiến thức: tìm hiểu về Hoisting, Event Loop, và cách JavaScript quản lý bộ nhớ qua Call Stack và Heap — tất cả đều có liên quan chặt chẽ đến Closure.

Bạn thấy bài viết hữu ích?

Liên hệ với chúng tôi để được tư vấn miễn phí về dịch vụ

Liên hệ ngay

Bài viết liên quan

Ảnh bìa bài viết: Hoisting Trong JavaScript: Cơ Chế Bí Ẩn Mà Mọi Dev Đều Phải Hiểu
Blockchain

Hoisting Trong JavaScript: Cơ Chế Bí Ẩn Mà Mọi Dev Đều Phải Hiểu

Bạn từng thấy code JavaScript gọi một hàm ở dòng trên cùng, trong khi định nghĩa hàm đó lại nằm tận cuối file — nhưng chương trình vẫn chạy bình thường? Hay bạn truy cập một biến trước khi khai báo và nhận được undefined thay vì báo lỗi? Đó là dấu hiệu của Hoisting. Nắm hiểu cơ chế này không chỉ giúp bạn xử lý tốt các câu hỏi phỏng vấn kỹ thuật mà còn là chìa khóa để hiểu cách JavaScript engine thực sự hoạt động bên dưới — trước khi thực thi bất kỳ dòng code nào.

29/3/2026
Ảnh bìa bài viết: useEffect vs useMemo: 5 Khác Biệt Quan Trọng Bạn Cần Hiểu Rõ
Frontend

useEffect vs useMemo: 5 Khác Biệt Quan Trọng Bạn Cần Hiểu Rõ

Nhầm lẫn giữa useEffect và useMemo là một trong những nguồn gốc phổ biến nhất gây ra các vấn đề hiệu suất trong ứng dụng React. Để dễ hình dung: hãy coi useMemo như một chiếc máy tính bỏ túi biết ghi nhớ kết quả, còn useEffect như một nhân viên trực tổng đài — chờ tín hiệu từ bên ngoài rồi mới hành động.

28/3/2026
Ảnh bìa bài viết: useMemo vs useCallback: 3 Bí Thuật Tối Ưu React Mà Senior Dev Áp Dụng
Frontend

useMemo vs useCallback: 3 Bí Thuật Tối Ưu React Mà Senior Dev Áp Dụng

Tối ưu hóa hiệu năng trong React thường bị coi là "vùng đất dữ" — nơi nhiều lập trình viên mắc kẹt với những quyết định sai lầm. Việc dùng useMemo và useCallback bừa bãi với hy vọng ứng dụng nhanh hơn đôi khi lại phản tác dụng, bắt CPU phải làm thêm việc so sánh dependencies không cần thiết. Bài viết này giúp bạn dùng hai hook này như một chuyên gia.

28/3/2026