Closure trong JavaScript: Đơn giản hơn bạn nghĩ

Closure nghe có vẻ hàn lâm, nhưng thực chất chỉ là khả năng một hàm “nhớ” môi trường nơi nó được tạo ra. Nhờ đó, bạn có thể giữ trạng thái riêng tư, viết API gọn gàng, và tránh vô số bug tinh vi. Bài viết này tóm tắt khái niệm, cách dùng thực tế, các mẫu code hay gặp và những hiểu lầm phổ biến để bạn nắm closure một cách chắc chắn.

Closure trong JavaScript: Đơn giản hơn bạn nghĩ

Closure là gì? Định nghĩa ngắn gọn

Mỗi khi JavaScript tạo một hàm, nó gắn kèm “môi trường từ vựng” (lexical environment) chứa các biến ở thời điểm khai báo. Khi hàm được gọi ở bất kỳ đâu sau này, nó vẫn có thể truy cập các biến đó. Tập hợp giữa hàm và môi trường “đi kèm” ấy được gọi là closure.

Ví dụ nhanh: bộ đếm có trạng thái riêng

function createCounter(start = 0) {
  let count = start;               // biến trong phạm vi từ vựng
  return {
    inc() { count++; return count; },
    dec() { count--; return count; },
    get() { return count; }
  };
}

const c1 = createCounter(10);
c1.inc();  // 11
c1.get();  // 11

const c2 = createCounter(); // c2 độc lập với c1
c2.inc();  // 1

Mỗi đối tượng trả về “đóng” trên biến count riêng của nó.

Vì sao closure hữu ích?

  • Đóng gói và ẩn dữ liệu: tạo “biến private” mà không cần class.
  • API gọn gàng: trả về hàm đã gắn cấu hình trước (currying/partial).
  • Callback “nhớ” ngữ cảnh: dùng trong event, timer, Promise.
  • Tối ưu hiệu năng: ghi nhớ kết quả tính toán (memoization).

Ba mẫu code thực dụng dùng closure

1. once: chỉ chạy hàm một lần

function once(fn) {
  let called = false, value;
  return function(...args) {
    if (!called) { value = fn.apply(this, args); called = true; }
    return value;
  };
}

const init = once(() => { /* khởi tạo tốn kém */ return "ok"; });
init(); // chạy thật
init(); // dùng lại kết quả

2. partial: ghim sẵn một phần tham số

function partial(fn, ...preset) {
  return (...later) => fn(...preset, ...later);
}

const add = (a,b,c) => a+b+c;
const add5 = partial(add, 2, 3);
add5(10); // 15

3. memoize: nhớ kết quả theo key

function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const val = fn.apply(this, args);
    cache.set(key, val);
    return val;
  };
}

const slowFib = n => n < 2 ? n : slowFib(n-1) + slowFib(n-2);
const fastFib = memoize(slowFib);
fastFib(40); // nhanh hơn đáng kể ở các lần sau

Closure, scope và var/let/const

Closure phụ thuộc vào phạm vi từ vựng. Điểm hay gặp lỗi nhất là vòng lặp với varvar có scope theo function, không theo block.

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

Cách đúng là dùng let (block scope) hoặc IIFE để “chốt” giá trị mỗi vòng:

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

// Cách 2: IIFE với var
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 0);
  })(i);
}

Bất đồng bộ và closure: setTimeout, event, Promise

Closure giúp callback “nhớ” biến đúng tại thời điểm khai báo, miễn là bạn dùng đúng scope.

function countdown(from) {
  for (let i = from; i >= 0; i--) {
    setTimeout(() => console.log(i), (from - i) * 1000);
  }
}
countdown(3); // 3,2,1,0 theo từng giây

Rò rỉ bộ nhớ vì closure? Hiểu cho đúng

  • Closure không tự gây rò rỉ. GC vẫn thu gom nếu không còn tham chiếu đến hàm/biến.
  • Dễ rò rỉ khi vô tình giữ tham chiếu dài hạn, ví dụ lưu callback vào một mảng global, hoặc gắn listener vào DOM mà không tháo.
  • Tháo listener khi không cần, tránh giữ cache vô hạn, và đặt thời hạn cho dữ liệu lưu trong closure.
// Ví dụ: nhớ tháo listener
function bind(el) {
  function onClick() { /* dùng closure */ }
  el.addEventListener('click', onClick);
  return () => el.removeEventListener('click', onClick);
}

Phân biệt this và closure

  • Closure “giữ” biến theo phạm vi từ vựng, không “giữ” this.
  • this phụ thuộc ngữ cảnh gọi hàm. Arrow function không có this riêng, nó dùng this của phạm vi ngoài gần nhất.
const obj = {
  x: 42,
  normal() { setTimeout(function(){ console.log(this.x); }, 0); }, // undefined
  arrow()  { setTimeout(() => { console.log(this.x); }, 0); }      // 42
};
obj.normal();
obj.arrow();

Debug và kiểm thử closure

  • Dùng DevTools Sources → Watch/Scope để xem biến trong Lexical environment.
  • Đặt breakpoint ngay trong hàm được trả về hoặc callback.
  • Kiểm thử hành vi: gọi nhiều lần và xác nhận trạng thái không bị lộ hoặc bị ghi đè sai.
// Kiểm thử đơn giản
const counter = (function() {
  let n = 0;
  return () => ++n;
})();
console.assert(counter() === 1);
console.assert(counter() === 2);

Các lỗi phổ biến cần tránh

  • Vòng lặp với var dẫn đến tất cả callback chia sẻ cùng một biến.
  • Giữ state trong closure quá lâu khiến khó quản lý vòng đời và test.
  • Memoize không có chiến lược xóa cache, làm phình bộ nhớ.
  • Nhầm lẫn giữa closure và ràng buộc this, dẫn đến giá trị this sai.

Tóm tắt nhanh

  • Closure = hàm + môi trường từ vựng tại thời điểm định nghĩa.
  • Dùng để ẩn dữ liệu, cấu hình trước hàm, quản lý callback bất đồng bộ, và tối ưu hiệu năng.
  • Ưu tiên let/const để tránh bẫy scope; quản lý vòng đời listener/cache để không rò rỉ.
  • Phân biệt rõ closure với this; arrow function giúp “ghim” this theo lexical scope.

Bình luận


  • Không có bình luận.

Init Toolbox

Nhấn Ctrl + \ trên máy tính, hoặc vuốt sang trái ở bất kỳ đâu trên mobile.

Đăng nhập





Đang tải...