Lesson 1.4.1 — Closure Formation
0. Metadata (Thông tin bài học)
| Field | Value |
|---|---|
| Stage | 1 |
| Module | 1.4 — Closures |
| Lesson | 1.4.1 — Closure Formation |
| Competency | C02 — JavaScript Runtime (C02.5 Closure) |
| Depth Target | L3 (Use) |
| Prerequisites | Scope (1.2.1), Lexical Environment (1.2.7), Variable Resolution (1.2.6), Function (S0) |
| Estimated Cognitive Load | High |
Out of Scope (Ngoài phạm vi)
- Garbage Collection internals (Stage 11)
- Stale Closure in React (Stage 8)
- Memory profiling (Stage 11)
thisbinding 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:
function createMultiplier(factor) {
return function (number) {
return number * factor;
};
}
const double = createMultiplier(2);
console.log(double(5)); // 10createMultiplier đã 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ể:
- Giải thích tại sao inner function truy cập được variable của outer function sau khi outer đã return.
- Vẽ closure environment diagram cho một đoạn code đơn giản.
- Dự đoán output của code có closure trong các trường hợp cơ bản.
- Implement một factory function sử dụng closure để tạo private state.
- 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:
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:
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
outerchứa binding:x → 10. innerđược tạo. Khi tạo,innerghi nhận tham chiếu đến environment củaouter(outer environment reference).
Step 2 — return inner:
innerđược trả về và gán cho biếnfnở global scope.inner"thoát" ra khỏiouter.
Step 3 — outer() kết thúc:
- Execution context của
outerbị pop khỏi call stack. - Tuy nhiên, environment của
outerkhông bị xóa vìfn(tứcinner) vẫn giữ tham chiếu đến nó.
Step 4 — fn() được gọi:
- Tạo Execution Context cho
fn/inner. innercần resolvex.- 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ấyx → 10. - In ra
10.
Step 5 — Kết luận:
innertruy cập đượcxkhô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
function makeCounter() {
let count = 0;
return function () {
count = count + 1;
return count;
};
}
const counter = makeCounter();
console.log(counter());
console.log(counter());Prediction 2
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
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:
1rồi2. - Giải thích:
countergiữ tham chiếu đến environment củamakeCounter. Mỗi lần gọicounter(),countđược cập nhật trong cùng một environment. Closure cho phépcounttồn tại và thay đổi qua nhiều lần gọi.
- Output:
Prediction 2:
- Output:
1rồi1. - Giải thích:
avàblà hai invocation riêng biệt củamakeCounter. Mỗi lần gọi tạo một environment mới.avàbgiữ tham chiếu đến hai environment khác nhau. Chúng không chia sẻcount.
- Output:
Prediction 3:
- Output:
3. - Giải thích:
outer()()trả vềinner.innergiữ reference xuyên qua hai cấp: environment củamiddle(chứay) và environment củaouter(chứax). Khiinnerchạy, nó tra cứuyở environment củamiddlerồixở environment củaouter. 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.
- Output:
Transfer Check — không cần biết API này:
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:
- Callback
() => console.log("changed")capture biến nào? - Tại sao callback vẫn chạy được sau khi đoạn code gọi
subscribeđã thực thi xong? unsubscriberất có thể làm gì với reference mà closure đang giữ?
Gợi ý trả lời
- 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ó.
- Vì callback giữ reference đến environment nơi nó được tạo — engine không xóa environment còn được tham chiếu.
- 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
makeGreeterreturn, 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}".
function makeGreeter(name) {
// TODO: return a function that uses `name`
}
const greetAlice = makeGreeter("Alice");
greetAlice(); // "Hello, Alice"[Đáp án tham khảo]
function makeGreeter(name) {
return function () {
console.log("Hello, " + name);
};
}- Giải thích: Function trả về capture
nametừ environment củamakeGreeter. MỗimakeGreeter("Alice")tạo một environment riêng vớinameriêng.
Lab 2 — Partial Scaffold: Tạo Multiplier
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]
function createMultiplier(factor) {
return function (number) {
return number * factor;
};
}- Giải thích:
factorđược capture một lần khicreateMultiplier(3)chạy. Mọi lần gọitriple(4)sau đó đều dùng lại cùng mộtfactortừ 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.
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getValue()); // 2
// counter.count phải là undefined[Đáp án tham khảo]
function createCounter() {
let count = 0;
return {
increment() {
count = count + 1;
},
getValue() {
return count;
},
};
}- Giải thích:
countchỉ tồn tại trong environment củacreateCounter. Các methodincrementvàgetValueđều là closure giữ tham chiếu đến environment đó. Không có cách nào truy cậpcounttừ 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
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
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
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):
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ặc3ba lần). - Không có lỗi thrown.
- Nếu đổi
varthànhlet, output đúng:0 1 2.
Hypothesis (Giả thuyết):
var icó 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:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
console.log("After loop:", i); // After loop: 3i 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ễ):
varkhông tạo block scope trong vòng lặpfor.- 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):
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}for (var i = 0; i < 3; i++) {
(function (capturedI) {
setTimeout(() => console.log(capturedI), 0);
})(i);
}Prevention (Phòng ngừa):
- Dùng
lettrong 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ó:
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:
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)
- Tự trả lờitrước: Viết ra giấy hoặc code: Closure là gì? Tại sao
innervẫn đọc đượcxsau khiouterreturn? - 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?"
- 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?
- 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
outerreturn, execution context bị pop, nhưng environment vẫn còn vìinnergiữ reference. innerkhô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
doublevẫn biếtfactorlà2sau khicreateMultiplierđã 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
numbervà 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ủacreateMultiplier. - Function bên trong được return và gán cho
double. Lúc nàycreateMultiplierđã chạy xong, execution context bị pop khỏi stack. Nhưng environment chứafactorkhông bị xóa vìdoublevẫn giữ tham chiếu đến nó. - Khi gọi
double(5), engine tạo execution context mới chodouble. Nó cần resolvefactor, không thấy trong environment của chính nó, nên đi theo outer environment reference — tìm thấyfactor = 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.
- Khi
💡 Hình dung như một căn phòng.
createMultipliermở một phòng, đểfactorvào đó, rồi đưa chìa khóa chodouble.createMultiplierra khỏi phòng, nhưng phòng không bị dọn vìdoublecò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ẻfactorkhô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ế closure | Teach Back (Truyền đạt lại) |
| Vẽ closure environment diagram | Lab 1 Bước 0 + Teach Back — diagram đúng cấu trúc |
| Dự đoán behavior của closure | Prediction (Dự đoán) 3/3 đúng |
| Implement private state | Implementation (Thực hành) — Lab 3 |
| Nhận biết closure hình thành / không | Edge Case 1 — phân tích captured variable |
| Nhận biết closure trong loop | Debug 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
createCountervớ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)