Skip to content
Home
Go back

Closure trong JavaScript: Cơ chế và ứng dụng thực tế

Mục lục

Xem mục lục

Closure là gì

Closure là khả năng một function “nhớ” được biến ở scope bao ngoài (outer scope), ngay cả khi scope đó đã kết thúc thực thi. Nói cách khác, inner function vẫn giữ được quyền truy cập vào biến của outer function, dù outer function đã return từ lâu.

Lexical Scope, nền tảng của Closure

Closure hình thành dựa trên Lexical Scope: phạm vi truy cập biến của một function được xác định bởi nơi function đó được khai báo trong code, không phải nơi nó được gọi.

function outer() {
  const message = "Xin chào";

  function inner() {
    console.log(message);
  }

  return inner;
}

inner được khai báo bên trong outer, nên theo lexical scope, inner luôn có quyền truy cập vào biến của outer, bất kể inner được gọi ở đâu sau này.

Ví dụ minh họa cơ bản

function outer() {
  const message = "Xin chào";

  function inner() {
    console.log(message);
  }

  return inner;
}

const fn = outer();
fn(); // "Xin chào"

outer() đã chạy xong và return trước khi fn() được gọi. Bình thường, biến cục bộ message sẽ bị hủy khi function chứa nó kết thúc. Nhưng vì inner (được gán cho fn) tham chiếu tới message, JavaScript giữ lại biến này thay vì hủy nó. Đây chính là closure.

Vì sao biến không bị garbage collector dọn

Garbage collector chỉ dọn một vùng nhớ khi không còn bất kỳ tham chiếu nào trỏ tới nó. Closure giữ tham chiếu trực tiếp đến biến, không phải bản sao giá trị tại thời điểm tạo.

function counter() {
  let count = 0;

  return function () {
    count += 1;
    return count;
  };
}

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

Mỗi lần gọi increment(), giá trị count được cập nhật và giữ nguyên cho lần gọi tiếp theo, vì closure tham chiếu tới cùng một biến count, không tạo bản sao mới mỗi lần gọi.

Cạm bẫy thường gặp: closure trong vòng lặp với var

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// In ra: 3, 3, 3

Nguyên nhân: var có function scope, không có block scope, nên chỉ tồn tại một biến i duy nhất dùng chung cho cả vòng lặp. Khi các callback trong setTimeout thực thi (sau khi vòng lặp đã chạy xong), chúng đều tham chiếu tới cùng biến i, lúc này đã có giá trị 3.

Cách khắc phục bằng let, vì let có block scope, mỗi vòng lặp tạo ra một biến i riêng:

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// In ra: 0, 1, 2

Trước khi có let (ES6), cách khắc phục phổ biến là dùng IIFE (Immediately Invoked Function Expression) để tạo scope riêng cho mỗi vòng lặp:

for (var i = 0; i < 3; i++) {
  (function (j) {
    setTimeout(() => console.log(j), 0);
  })(i);
}
// In ra: 0, 1, 2

Ứng dụng thực tế

Data privacy / Module pattern

Closure cho phép tạo biến “private”, không thể truy cập trực tiếp từ bên ngoài, chỉ thao tác được qua các function được expose:

function createBankAccount(initialBalance) {
  let balance = initialBalance;

  return {
    deposit(amount) {
      balance += amount;
      return balance;
    },
    getBalance() {
      return balance;
    },
  };
}

const account = createBankAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 150
console.log(account.balance); // undefined, không truy cập trực tiếp được

Memoization

Closure giữ lại cache giữa các lần gọi, tránh tính toán lại kết quả đã có:

function memoize(fn) {
  const cache = new Map();

  return function (arg) {
    if (cache.has(arg)) {
      return cache.get(arg);
    }
    const result = fn(arg);
    cache.set(arg, result);
    return result;
  };
}

Currying

Closure cho phép một function trả về function khác, giữ lại các tham số đã truyền trước đó:

function multiply(a) {
  return function (b) {
    return a * b;
  };
}

const double = multiply(2);
console.log(double(5)); // 10

Debounce / Throttle

Closure giữ lại trạng thái (timer, thời điểm gọi gần nhất) giữa các lần gọi function:

function debounce(fn, delay) {
  let timer;

  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

Đánh đổi cần lưu ý

Closure giữ tham chiếu biến trong suốt thời gian còn được sử dụng, nghĩa là biến đó không được giải phóng bộ nhớ sớm. Trong phần lớn trường hợp điều này không đáng lo, nhưng có thể gây memory leak nếu:

Cách giảm rủi ro: gỡ event listener khi không còn cần (removeEventListener), tránh giữ closure sống lâu hơn mức cần thiết, và tách riêng biến thực sự cần giữ lại thay vì để closure tham chiếu tới cả một scope lớn.

Tổng kết


Share this post:

Previous Post
Event Loop: Call Stack, Task Queue và Microtask Queue
Next Post
Từ khóa `this` trong JavaScript: Cách xác định giá trị