Skip to content

Lesson 1.4.1 — Closure Formation ​

Bài 1.4.1 — Closure Formation
Function Creation, Lexical Environment & Closure Formation • 24 phút
0:00 / 0:00

0. Metadata (Thông tin bài học) ​

FieldValue
Stage1
Module1.4 — Closures
Lesson1.4.1 — Closure Formation
CompetencyC02 — JavaScript Runtime (C02.5 Closure)
Depth TargetL3 (Use)
PrerequisitesScope (1.2.1), Lexical Environment (1.2.7), Variable Resolution (1.2.6), Function (S0)
Estimated Cognitive LoadHigh

Out of Scope (Ngoài phạm vi)

  • Garbage Collection internals (Stage 11)
  • Stale Closure in React (Stage 8)
  • Memory profiling (Stage 11)
  • this binding inside closure (Stage 2)

1. Why This Exists (Vì sao cần học) ​

Bạn viết một hàm để tạo ra một hàm khác:

js
function createMultiplier(factor) {
  return function (number) {
    return number * factor;
  };
}

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

createMultiplier đã chạy xong và trả về kết quả. Theo logic thông thường, biến factor đã "biến mất" cùng với hàm. Vậy tại sao double(5) vẫn biết factor là 2?

Nếu không hiểu cơ chế này, bạn sẽ không thể:

  • Tạo private state trong JavaScript.
  • Hiểu tại sao callback giữ lại dữ liệu.
  • Debug các bug kinh điển như loop + callback.
  • Hiểu React Hooks (Stage 8).

Đây là bài học quan trọng nhất trong Stage 1.

2. Prerequisites (Yêu cầu đầu vào) ​

Trước khi học bài này, bạn phải:

  • [ ] Phân biệt được Global Scope, Function Scope, Block Scope.
  • [ ] Hiểu Lexical Environment lưu bindings và tham chiếu đến outer environment.
  • [ ] Trace được Variable Resolution qua Scope Chain.
  • [ ] Biết Execution Context được tạo khi function được gọi và bị xóa khi return.

Nếu thiếu, quay lại Module 1.2 trước.

3. Learning Objectives (Mục tiêu học tập) ​

Sau bài này, bạn có thể:

  1. Giải thích tại sao inner function truy cập được variable của outer function sau khi outer đã return.
  2. Vẽ closure environment diagram cho một đoạn code đơn giản.
  3. Dự đoán output của code có closure trong các trường hợp cơ bản.
  4. Implement một factory function sử dụng closure để tạo private state.
  5. Nhận biết khi nào closure được hình thành và khi nào không.

4. Mental Model (Mô hình tư duy) ​

Mental Model — Closure Formation

Khi một function được tạo bên trong một function khác, nó "ghi nhớ" environment nơi nó được sinh ra. Nếu function bên trong thoát ra ngoài (return, gán cho biến, truyền vào callback), nó mang theo tham chiếu đến environment của function bên ngoài. Environment đó không bị xóa vì vẫn có thứ đang tham chiếu đến nó.

Flow từ quá trình hình thành:

text
Outer Function Called
        ↓
Outer Environment Created (chứa local bindings)
        ↓
Inner Function Created + ghi nhận Outer Environment Reference
        ↓
Inner Function Escapes (return / assign / pass)
        ↓
Outer Execution Ends — nhưng Environment vẫn còn vì Inner giữ reference
        ↓
Inner Called Later → tra cứu variable trong Outer Environment

Điều quan trọng: Closure không phải là một "túi" dữ liệu được copy. Closure là một tham chiếu đến environment đã tồn tại.

5. Core Concepts (Các khái niệm cốt lõi) ​

Essential (Bắt buộc) ​

  • Closure: Một function kết hợp với lexical environment mà nó được tạo ra.
  • Captured Variable: Biến của outer scope mà inner function tham chiếu đến.
  • Environment Retention: Outer environment không bị giải phóng nếu vẫn có inner function tham chiếu.

Supporting (Hỗ trợ) ​

  • Function return một function khác.
  • Variable lookup qua scope chain.

Awareness (Biết tồn tại) ​

  • Engine có thể tối ưu bằng cách chỉ giữ lại captured variables thay vì toàn bộ environment.

6. Worked Example (Ví dụ phân tích từng bước) ​

Worked Example — Trace từng bước:

js
function outer() {
  const x = 10;

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

  return inner;
}

const fn = outer();
fn();

Step 1 — outer() được gọi:

  • Tạo Function Execution Context cho outer.
  • Environment của outer chứa binding: x → 10.
  • inner được tạo. Khi tạo, inner ghi nhận tham chiếu đến environment của outer (outer environment reference).

Step 2 — return inner:

  • inner được trả về và gán cho biến fn ở global scope.
  • inner "thoát" ra khỏi outer.

Step 3 — outer() kết thúc:

  • Execution context của outer bị pop khỏi call stack.
  • Tuy nhiên, environment của outer không bị xóa vì fn (tức inner) vẫn giữ tham chiếu đến nó.

Step 4 — fn() được gọi:

  • Tạo Execution Context cho fn/inner.
  • inner cần resolve x.
  • Tra cứu trong environment của inner: không có x.
  • Theo outer environment reference, tra cứu trong environment của outer: tìm thấy x → 10.
  • In ra 10.

Step 5 — Kết luận:

  • inner truy cập được x không phải vì "copy" mà vì "giữ tham chiếu đến environment cũ".

7. Prediction Exercise (Bài tập dự đoán) ​

Đừng chạy code. Dự đoán output và giải thích tại sao.

Prediction 1

js
function makeCounter() {
  let count = 0;
  return function () {
    count = count + 1;
    return count;
  };
}

const counter = makeCounter();
console.log(counter());
console.log(counter());

Prediction 2

js
function makeCounter() {
  let count = 0;
  return function () {
    count = count + 1;
    return count;
  };
}

const a = makeCounter();
const b = makeCounter();
console.log(a());
console.log(b());

Prediction 3

js
function outer() {
  const x = 1;
  return function middle() {
    const y = 2;
    return function inner() {
      return x + y;
    };
  };
}

console.log(outer()()());
[Đáp án & Giải thích]
  • Prediction 1:

    • Output: 1 rồi 2.
    • Giải thích: counter giữ tham chiếu đến environment của makeCounter. Mỗi lần gọi counter(), count được cập nhật trong cùng một environment. Closure cho phép count tồn tại và thay đổi qua nhiều lần gọi.
  • Prediction 2:

    • Output: 1 rồi 1.
    • Giải thích: a và b là hai invocation riêng biệt của makeCounter. Mỗi lần gọi tạo một environment mới. a và b giữ tham chiếu đến hai environment khác nhau. Chúng không chia sẻ count.
  • Prediction 3:

    • Output: 3.
    • Giải thích: outer()() trả về inner. inner giữ reference xuyên qua hai cấp: environment của middle (chứa y) và environment của outer (chứa x). Khi inner chạy, nó tra cứu y ở environment của middle rồi x ở environment của outer. Chứng minh closure capture không giới hạn ở một cấp scope — mỗi function giữ reference đến toàn bộ chuỗi lexical environment nơi nó được tạo.

Transfer Check — không cần biết API này:

js
const unsubscribe = store.subscribe(() => console.log("changed"));

Bạn chưa học store.subscribe là gì. Chỉ dùng mental model closure: subscribe nhận một function và trả về unsubscribe. Hãy trả lời:

  1. Callback () => console.log("changed") capture biến nào?
  2. Tại sao callback vẫn chạy được sau khi đoạn code gọi subscribe đã thực thi xong?
  3. unsubscribe rất có thể làm gì với reference mà closure đang giữ?
Gợi ý trả lời
  1. Có thể không capture biến nào — closure vẫn hình thành vì mọi function đều giữ reference đến lexical environment của nó.
  2. Vì callback giữ reference đến environment nơi nó được tạo — engine không xóa environment còn được tham chiếu.
  3. Rất có thể gỡ callback ra khỏi danh sách listener — sau đó callback không còn được gọi, và nếu không thứ gì giữ reference nữa, environment mới có thể được giải phóng.

8. Implementation Lab (Bài lab thực hành) ​

Lab 1 — Guided: Tạo Greeter ​

Bước 0 — Vẽ diagram trước khi code. Trước khi viết bất kỳ dòng nào, vẽ closure environment diagram cho makeGreeter("Alice"):

  • Environment nào được tạo khi makeGreeter("Alice") chạy?
  • Binding nào tồn tại trong environment đó?
  • Sau khi makeGreeter return, reference nào vẫn được giữ lại, bởi ai?

So sánh diagram của bạn với Walkthrough ở Section 6. Nếu diagram sai, code sẽ sai theo — đây chính là lý do ta vẽ trước khi code.

Viết một function makeGreeter(name) trả về một function. Function trả về khi được gọi sẽ in ra "Hello, {name}".

js
function makeGreeter(name) {
  // TODO: return a function that uses `name`
}

const greetAlice = makeGreeter("Alice");
greetAlice(); // "Hello, Alice"
[Đáp án tham khảo]
js
function makeGreeter(name) {
  return function () {
    console.log("Hello, " + name);
  };
}
  • Giải thích: Function trả về capture name từ environment của makeGreeter. Mỗi makeGreeter("Alice") tạo một environment riêng với name riêng.

Lab 2 — Partial Scaffold: Tạo Multiplier ​

js
function createMultiplier(factor) {
  return function (number) {
    // TODO: dùng `factor` từ outer environment để nhân với `number`
    // Gợi ý: không cần khai báo lại `factor` — nó đã nằm trong environment
  };
}

const triple = createMultiplier(3);
console.log(triple(4)); // 12
[Đáp án tham khảo]
js
function createMultiplier(factor) {
  return function (number) {
    return number * factor;
  };
}
  • Giải thích: factor được capture một lần khi createMultiplier(3) chạy. Mọi lần gọi triple(4) sau đó đều dùng lại cùng một factor từ cùng một environment.

Lab 3 — Independent: Private Counter ​

Tạo createCounter() trả về một object có hai method: increment() và getValue(). Không được dùng class hoặc object property để lưu count. count phải hoàn toàn private — không thể truy cập từ bên ngoài.

js
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getValue()); // 2
// counter.count phải là undefined
[Đáp án tham khảo]
js
function createCounter() {
  let count = 0;
  return {
    increment() {
      count = count + 1;
    },
    getValue() {
      return count;
    },
  };
}
  • Giải thích: count chỉ tồn tại trong environment của createCounter. Các method increment và getValue đều là closure giữ tham chiếu đến environment đó. Không có cách nào truy cập count từ bên ngoài object.

9. Edge Cases (Các trường hợp ngoại lệ) ​

Edge Case 1 — Không có captured variable ​

js
function outer() {
  const x = 10;
  function inner() {
    console.log("hello");
  }
  return inner;
}

Phân tích

inner không tham chiếu đến x. Mặc dù vẫn có outer environment reference, engine có thể tối ưu và không giữ lại x. Đây là implementation detail, nhưng mental model vẫn đúng: closure chỉ "cần" những gì nó dùng.

Edge Case 2 — Gọi outer nhiều lần = nhiều environment ​

js
function outer() {
  const x = Math.random();
  return function () {
    return x;
  };
}

const a = outer();
const b = outer();

Phân tích

a và b giữ tham chiếu đến hai environment hoàn toàn khác nhau. a() và b() trả về hai giá trị khác nhau. Đây là cách factory function hoạt động.

Edge Case 3 — Reassign captured variable ​

js
function outer() {
  let x = 10;
  return {
    get: () => x,
    set: (v) => {
      x = v;
    },
  };
}

Phân tích

Cả get và set đều là closure cùng tham chiếu đến một x. set thay đổi x trong environment, và get đọc giá trị mới. Closure không chỉ "đọc" — nó cho phép "đọc và ghi" vào environment đã capture.

10. Debug Lab (Bài lab gỡ lỗi) ​

Debug Lab — Closure trong Loop

Symptom (Triệu chứng): Developer mong đợi in ra 0 1 2 nhưng kết quả là 3 3 3.

Reproduction (Tái hiện lỗi):

js
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 0);
}

Evidence (Bằng chứng):

  • Output thực tế: 3 3 3 (hoặc 3 ba lần).
  • Không có lỗi thrown.
  • Nếu đổi var thành let, output đúng: 0 1 2.

Hypothesis (Giả thuyết):

  • var i có function scope (hoặc global scope), không phải block scope.
  • Cả ba callback đều capture cùng một biến i.
  • Khi callback chạy (sau khi loop kết thúc), i đã là 3.

Verification (Xác minh): Thêm log ngay sau loop:

js
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
console.log("After loop:", i); // After loop: 3

i là 3 khi callback chạy. Cả ba closure đều tham chiếu đến cùng một i.

Root Cause (Nguyên nhân gốc rễ):

  • var không tạo block scope trong vòng lặp for.
  • Tất cả callback đều là closure cùng tham chiếu đến một binding duy nhất.
  • Callback chạy sau khi loop đã hoàn thành, nên đọc giá trị cuối cùng.

Fix (Sửa):

js
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
js
for (var i = 0; i < 3; i++) {
  (function (capturedI) {
    setTimeout(() => console.log(capturedI), 0);
  })(i);
}

Prevention (Phòng ngừa):

  • Dùng let trong vòng lặp khi tạo callback.
  • Luôn hỏi: "Callback này capture biến nào? Biến đó có thay đổi trước khi callback chạy không?"

11. Design Exercise (Bài tập thiết kế giải pháp) ​

Không áp dụng ở depth L3. Design Exercise sẽ xuất hiện ở Lesson 1.4.3 (Private State Design) và các lesson L5–L6 sau này.

12. Production Scenario (Tình huống thực tế) ​

Production Scenario — Event Handler trong Loop

Bạn đang viết một dashboard có 3 nút. Mỗi nút khi click phải log ra index của nó:

js
const buttons = document.querySelectorAll("button");

for (var i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener("click", () => {
    console.log("Clicked button", i);
  });
}

Vấn đề: Dù click nút nào, kết quả luôn là index cuối cùng.

Tại sao: Tất cả event listener đều là closure cùng tham chiếu đến một biến i duy nhất ở outer scope. Khi click xảy ra, loop đã kết thúc và i đã đạt giá trị cuối.

Cách sửa:

js
for (let i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener("click", () => {
    console.log("Clicked button", i);
  });
}

let tạo một binding mới cho mỗi iteration, nên mỗi listener capture một i riêng biệt.

13. AI-assisted Exercise (Bài tập với AI) ​

Level B — Challenge (Thách thức)

  1. Tự trả lờitrước: Viết ra giấy hoặc code: Closure là gì? Tại sao inner vẫn đọc được x sau khi outer return?
  2. Hỏi AI: Dùng prompt: "Explain JavaScript closures in simple terms. Why can an inner function access outer variables after the outer function returns?"
  3. So sánh: So sánh câu trả lời AI với câu trả lời củbạn. AI có nhắc đến "environment reference" không? AI có phân biệt "copy" và "reference" không?
  4. Verify: Kiểm tra lại bằng MDN (search "MDN Closure") hoặc ECMAScript Specification (Environment Records).

Gợi ý

Để ý xem AI có dùng từ mơ hồ như "function nhớ scope" hay "JavaScript lưu lại biến" không. Những từ này đúng ở bề mặt nhưng không giải thích được cơ chế. Mental model đúng phải đề cập đến environment và reference.

Đáp án tham khảo
  • Bạn nghĩ:

    • Closure là inner function giữ tham chiếu đến lexical environment của outer function.
    • Khi outer return, execution context bị pop, nhưng environment vẫn còn vì inner giữ reference.
    • inner không "copy" giá trị mà "tra cứu" qua outer environment reference.
  • AI trả lời:

    • AI thường nói: "Closure là function có thể truy cập biến của outer function ngay cả sau khi outer function đã thực thi xong."
    • AI có thể nói: "JavaScript lưu lại scope chain để function sau này vẫn dùng được."
    • AI hiếm khi phân biệt rõ: execution context vs environment, hoặc giải thích tại sao environment không bị xóa.
  • So sánh:

    • AI đúng về kết quả (inner vẫn truy cập được), nhưng thiếu depth về cơ chế.
    • AI thường không nhắc đến "environment reference" — mà đây chính là mental model cốt lõi.
    • AI có thể gây hiểu nhầm là JavaScript "copy" toàn bộ scope, trong khi thực tế là giữ reference.
  • Điểm AI nói sai hoặc quá mơ hồ:

    • "Lưu lại scope chain" — mơ hồ, không giải thích tại sao scope chain không bị xóa.
    • "Function nhớ biến" — đúng nhưng không causal; không giải thích cách nhớ.
    • Thường bỏ sót sự khác biệt giữa mỗi lần gọi outer function tạo ra environment riêng.
  • Kết luận:

    • Nếu bạn chỉ ra được AI thiếu "environment reference" và "reference retention", bạn đã hiểu sâu hơn AI.
    • Concept này sẽ quay lại ở Stage 3 (Async Callback), Stage 8 (React Stale Closure), và Stage 11 (Memory Retention). :::

14. Teach Back (Dạy lại) ​

Giải thích cho một đồng nghiệp junior trong 2 phút:

"Tại sao function double vẫn biết factor là 2 sau khi createMultiplier đã chạy xong?"

Yêu cầu:

  • Dùng đúng terminology: environment, reference, capture.
  • Không được trả lời bằng intuition kiểu "vì nó nhớ".
  • Vẽ hoặc mô tả mental model bằng lời.
Mô phỏng
  • Bạn nói:
    • Khi createMultiplier(2) chạy, engine tạo một execution context và một lexical environment cho nó. Trong environment đó, factor được bind với giá trị 2.
    • Function bên trong — cái nhận number và trả về number * factor — được tạo ra trong environment này. Khi tạo, nó ghi nhận một tham chiếu đến environment của createMultiplier.
    • Function bên trong được return và gán cho double. Lúc này createMultiplier đã chạy xong, execution context bị pop khỏi stack. Nhưng environment chứa factor không bị xóa vì double vẫn giữ tham chiếu đến nó.
    • Khi gọi double(5), engine tạo execution context mới cho double. Nó cần resolve factor, không thấy trong environment của chính nó, nên đi theo outer environment reference — tìm thấy factor = 2 ở environment cũ. Kết quả là 10.
    • Điểm then chốt: closure không phải là "copy dữ liệu", mà là "giữ tham chiếu đến environment". Nếu gọi createMultiplier(2) hai lần, ta có hai environment khác nhau, và hai closure khác nhau.

💡 Hình dung như một căn phòng. createMultiplier mở một phòng, để factor vào đó, rồi đưa chìa khóa cho double. createMultiplier ra khỏi phòng, nhưng phòng không bị dọn vì double còn giữ chìa khóa.

Gợi ý đánh giá bản thân

  • Đồng nghiệp có hiểu tại sao environment không bị xóa không?
  • Bạn có giải thích được tại sao a = createMultiplier(2) và b = createMultiplier(3) không chia sẻ factor không?
  • Nếu đồng nghiệp hỏi "vậy memory có bị leak không?", bạn có trả lời được ở mức conceptual không? (Hint: environment vẫn còn vì có reference, đó là expected behavior, chưa phải leak.)

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giá
Giải thích cơ chế closureTeach Back (Truyền đạt lại)
Vẽ closure environment diagramLab 1 Bước 0 + Teach Back — diagram đúng cấu trúc
Dự đoán behavior của closurePrediction (Dự đoán) 3/3 đúng
Implement private stateImplementation (Thực hành) — Lab 3
Nhận biết closure hình thành / khôngEdge Case 1 — phân tích captured variable
Nhận biết closure trong loopDebug Lab — xác định root cause

16. Exit Criteria (Tiêu chí qua bài) ​

  • [ ] Có thể giải thích tại sao inner function truy cập được outer variable sau khi outer return.
  • [ ] Có thể vẽ closure environment diagram cho code 1 cấp nested.
  • [ ] Dự đoán đúng 3/3 prediction scenarios.
  • [ ] Implement được createCounter với private state bằng closure.
  • [ ] Debug được loop closure bug và giải thích bằng environment model.
  • [ ] Phân biệt được "copy giá trị" vs "giữ reference đến environment".

17. Spiral Connection (Liên kết xoắn ốc) ​

Previous (Trước): Scope → Lexical Environment → Variable Resolution. Bạn đã biết engine tìm biến bằng cách đi ngược scope chain.

Current (Hiện tại): Closure Formation. Bạn học rằng khi inner function thoát ra ngoài, nó mang theo tham chiếu đến outer environment. Đây là ứng dụng trực tiếp của lexical scope + environment reference.

Next (Tiếp theo):

  • Lesson 1.4.2 — Closure & Lifetime (environment tồn tại bao lâu?)
  • Lesson 1.4.3 — Closure làm Private State (ứng dụng thực tế)
  • Stage 3 — Async Callbacks (closure giữ state qua thờgian)
  • Stage 8 — React Hooks (stale closure, useEffect dependencies)
  • Stage 11 — Memory & Debugging (retained references, memory leaks)
📴 Offline Mode — Content served from cache