Lesson 1.4.2 — Closure và Lifetime
0. Metadata (Thông tin bài học)
| Field | Value |
|---|---|
| Stage | 1 |
| Module | 1.4 — Closures |
| Lesson | 1.4.2 — Closure và Lifetime |
| Competency | C02 — JavaScript Runtime (C02.5 Closure) |
| Depth Target | L4 (Debug) |
| Prerequisites | Closure Formation (1.4.1), Lexical Environment (1.2.7), Variable Resolution (1.2.6), Execution Context Stack (1.1.5) |
| Estimated Cognitive Load | High |
Out of Scope (Ngoài phạm vi)
- Garbage Collection algorithms và memory profiling (Stage 11)
- Weak references,
WeakMap,WeakRef,FinalizationRegistry(Stage 11) - Stale Closure (Lesson 1.4.8)
- Async scheduling / Event Loop (Stage 3)
- React lifecycle và Hooks (Stage 8)
1. Why This Exists (Vì sao cần học)
Ở Lesson 1.4.1, bạn đã thấy inner function vẫn truy cập được outer variable sau khi outer function return:
function outer() {
const message = "hello";
return function inner() {
return message;
};
}
const fn = outer();
console.log(fn()); // "hello"Nhưng còn một câu hỏi sâu hơn:
outer()đã kết thúc. Execution Context củaouterđã bị pop khỏi Call Stack. Vậy dữ liệu màinnercần đang tồn tại ở đâu, và tồn tại đến bao lâu?
Nếu chỉ ghi nhớ câu "closure nhớ biến", bạn sẽ dễ hình thành mental model sai rằng:
- Function return thì mọi local variable phải bị xóa ngay.
- Closure copy giá trị sang một vùng memory riêng.
- Outer environment tồn tại vĩnh viễn sau khi có closure.
- Execution Context và Lexical Environment có cùng lifetime.
- Một closure tồn tại đồng nghĩa chắc chắn có memory leak.
Lesson này tách riêng execution lifetime khỏi environment reachability. Đây là bước đầu để sau này bạn reasoning được callback lifecycle, stale closure và memory retention bằng cùng một mental model.
2. Prerequisites (Yêu cầu đầu vào)
Trước khi học bài này, bạn phải:
- [ ] Giải thích được Closure Formation ở Lesson 1.4.1.
- [ ] Biết inner function giữ reference cần thiết đến lexical environment nơi nó được tạo.
- [ ] Phân biệt Execution Context với Lexical Environment.
- [ ] Biết Function Execution Context được push khi function được gọi và pop khi function kết thúc.
- [ ] Trace được Variable Resolution từ current environment ra outer environment.
Nếu chưa chắc, quay lại Lesson 1.4.1 và Module 1.1–1.2 trước.
3. Learning Objectives (Mục tiêu học tập)
Sau bài này, bạn có thể:
- Phân biệt lifetime của Execution Context với lifetime của environment được closure giữ lại.
- Giải thích tại sao outer function đã return nhưng captured binding vẫn còn reachable.
- Vẽ reference/lifetime diagram cho một closure đơn giản.
- Dự đoán khi nào hai closures cùng giữ một environment và khi nào chúng giữ environment khác nhau.
- Debug một tình huống trong đó dữ liệu vẫn còn reachable vì một closure vẫn đang được giữ reference.
- Nhận biết điều kiện conceptual để environment có thể không còn cần được giữ lại.
4. Mental Model (Mô hình tư duy)
Mental Model — Closure Lifetime
Execution Context và Lexical Environment không nên được xem là cùng một thứ có cùng lifetime.
Khi outer function kết thúc, Execution Context của nó rời Call Stack. Nhưng nếu một function còn reachable vẫn cần bindings từ outer lexical environment, environment cần thiết đó vẫn còn reachable thông qua closure.
Flow:
outer() called
↓
Execution Context + Environment created
↓
inner function created
↓
inner keeps needed outer environment reference
↓
outer() returns inner
↓
Outer Execution Context leaves Call Stack
↓
Returned inner is still reachable
↓
Needed outer environment remains reachableĐiểm quan trọng:
Function execution kết thúc không đồng nghĩa mọi dữ liệu liên quan tới invocation đó lập tức biến mất.
Ví dụ:
function createReader() {
const value = 42;
return function read() {
return value;
};
}
const reader = createReader();Sau khi createReader() return:
Global Environment
└── reader
↓
Function read
↓
Outer Environment
└── value = 42createReader không còn đang chạy. Nhưng reader vẫn reachable từ global binding, và function read vẫn cần value. Vì vậy environment chứa binding cần thiết vẫn còn reachable.
Common Misconception
Không nên nói:
"Closure làm outer function sống mãi."
Outer function không còn đang execute. Thứ được giữ lại là environment/bindings cần thiết mà closure vẫn có thể reach.
Cũng không nên kết luận engine bắt buộc phải giữ nguyên toàn bộ environment theo đúng hình vẽ. Engine có thể tối ưu implementation. Diagram ở đây là mental model để reasoning về observable behavior.
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
- Execution Lifetime: Khoảng thời gian một Function Execution Context đang tồn tại trên Call Stack.
- Environment Reachability: Environment vẫn có thể được truy cập qua một chain/reference còn reachable.
- Retained Environment: Outer environment vẫn cần tồn tại ở mức observable behavior vì closure còn cần bindings của nó.
- Closure Lifetime: Gắn với việc function object còn reachable và còn giữ reference cần thiết đến outer environment.
- Reference Chain: Một chuỗi reference có thể khiến function và environment tiếp tục reachable.
Supporting (Hỗ trợ)
- Return một function có thể làm function đó escape khỏi outer invocation.
- Gán closure vào variable, object hoặc collection có thể giữ closure reachable.
- Nhiều closures có thể cùng tham chiếu đến một outer environment.
- Khi không còn reference reachable tới closure, environment mà chỉ closure đó giữ có thể trở nên unreachable.
Awareness (Biết tồn tại)
- JavaScript engine có Garbage Collector xử lý memory không còn reachable.
- Engine có thể tối ưu cách lưu captured bindings.
- "Không còn reachable" không có nghĩa bạn có thể dự đoán chính xác thời điểm memory được thu hồi.
6. Worked Example (Ví dụ phân tích từng bước)
Worked Example — Execution kết thúc nhưng Environment vẫn reachable:
function createAccount(name) {
const createdAt = Date.now();
function describe() {
return `${name} - ${createdAt}`;
}
return describe;
}
const accountInfo = createAccount("Alice");
console.log(accountInfo());Step 1 — createAccount("Alice") được gọi:
- Tạo Function Execution Context cho
createAccount. - Tạo lexical environment chứa:
name → "Alice"createdAt → <timestamp>- binding cho
describe
- Function
describeđược tạo trong lexical environment này.
Step 2 — return describe:
- Function object
describeđược trả về. - Global binding
accountInfogiữ reference đến function đó. describecần resolvenamevàcreatedAttừ outer environment.
Step 3 — createAccount kết thúc:
- Execution Context của
createAccountbị pop khỏi Call Stack. - Không còn statement nào của
createAccountđang execute. - Nhưng environment chứa
namevàcreatedAtvẫn cần reachable vìaccountInfocòn giữ functiondescribe.
Step 4 — accountInfo() được gọi sau đó:
- Tạo Execution Context mới cho
describe. describecầnnamevàcreatedAt.- Variable Resolution đi qua outer environment reference.
- Tìm thấy các bindings thuộc invocation cũ của
createAccount.
Step 5 — Kết luận:
createAccount execution
↓
ended
accountInfo reference
↓
describe function
↓
outer environment
↓
name + createdAtĐiều tồn tại sau return không phải "Execution Context cũ vẫn nằm trên stack". Thứ còn reachable là environment mà closure vẫn cần.
7. Prediction Exercise (Bài tập dự đoán)
Đừng chạy code. Dự đoán output và giải thích bằng reference + environment lifetime.
Prediction 1
function outer() {
let value = 1;
return function () {
value = value + 1;
return value;
};
}
const fn = outer();
console.log(fn());
console.log(fn());Prediction 2
function outer() {
const value = Math.random();
return function () {
return value;
};
}
const a = outer();
const b = outer();
console.log(a() === b());Prediction 3
function outer() {
const value = 10;
return {
readA() {
return value;
},
readB() {
return value * 2;
},
};
}
const api = outer();
console.log(api.readA());
console.log(api.readB());[Đáp án & Giải thích]
Prediction 1:
- Output:
2rồi3. - Giải thích:
fnvẫn reachable và giữ reference cần thiết đến environment của một invocationouter. Bindingvaluetrong environment đó tiếp tục được đọc và cập nhật qua nhiều lần gọifn.
- Output:
Prediction 2:
- Kết quả thông thường là
falsevì hai lần gọiouter()tạo hai invocation và hai environment khác nhau, mỗi environment giữ một kết quảMath.random()riêng. - Điểm cần reasoning:
akhông giữ environment củab, và ngược lại.
- Kết quả thông thường là
Prediction 3:
- Output:
10rồi20. readAvàreadBđược tạo trong cùng một invocationouter, nên cả hai cùng có thể resolve bindingvaluecủa cùng outer environment.
- Output:
Transfer Check — reference bị giữ ở nơi khác:
const handlers = [];
function register() {
const id = "user-42";
function handler() {
return id;
}
handlers.push(handler);
}
register();Không cần biết một framework hay event system nào. Chỉ dùng reference model và trả lời:
- Sau khi
register()return,handlercòn reachable không? - Reference nào đang giữ
handler? idcòn cần reachable không?- Nếu
handlerskhông còn giữhandlervà không có reference nào khác tới nó, điều gì có thể xảy ra với environment chỉ được closure đó giữ?
Gợi ý trả lời
- Có.
handlervẫn reachable. - Array
handlersgiữ reference đến functionhandler. - Có.
handlervẫn có thể được gọi và cần resolveid. - Environment có thể trở nên unreachable nếu thực sự không còn reference chain nào cần nó. Thời điểm memory được reclaim là implementation detail của Garbage Collector và không thuộc phạm vi lesson này.
8. Implementation Lab (Bài lab thực hành)
Lab 1 — Guided: Quan sát lifetime bằng API đơn giản
Bước 0 — Vẽ diagram trước khi code.
Với code:
const session = createSession("Alice");Hãy vẽ:
session
↓
returned function
↓
outer environment
↓
nameSau đó implement:
function createSession(name) {
// TODO
}
const session = createSession("Alice");
console.log(session()); // "Session: Alice"
console.log(session()); // "Session: Alice"Yêu cầu:
createSessionphải return một function.- Function trả về phải đọc
nametừ outer environment. - Không copy
namesang global variable.
[Đáp án tham khảo]
function createSession(name) {
return function () {
return `Session: ${name}`;
};
}- Giải thích: Sau khi
createSessionkết thúc, returned function vẫn reachable quasession. Function đó vẫn cần bindingname, nên environment cần thiết của invocation vẫn reachable.
Lab 2 — Partial Scaffold: Hai closures cùng một lifetime source
Hoàn thiện:
function createState(initialValue) {
let value = initialValue;
return {
read() {
// TODO
},
write(nextValue) {
// TODO
},
};
}
const state = createState(10);
console.log(state.read()); // 10
state.write(20);
console.log(state.read()); // 20Sau khi implement, trả lời:
readvàwritecó giữ haivaluekhác nhau không?- Khi
createStatereturn, vì saovaluevẫn accessible? - Nếu chỉ còn
readreachable màwritekhông còn được giữ,valuevẫn cần tồn tại không?
[Đáp án tham khảo]
function createState(initialValue) {
let value = initialValue;
return {
read() {
return value;
},
write(nextValue) {
value = nextValue;
},
};
}readvàwritecùng được tạo trong một invocation nên cùng resolve một bindingvalue.- Chỉ cần một closure còn reachable và còn cần
value, binding đó vẫn cần observable. - Nếu
readvẫn reachable và vẫn dùngvalue, environment chứavaluevẫn cần reachable.
Lab 3 — Independent: Release một reference
Tạo:
function createReader(value) {
// implement
}
let reader = createReader("secret");
console.log(reader()); // "secret"
reader = null;Viết một đoạn giải thích 4–6 câu, không cần đo memory:
- Trước
reader = null, reference chain nào giữ returned function reachable? - Returned function giữ outer binding nào?
- Sau
reader = null, bạn có được phép khẳng định memory bị giải phóng ngay lập tức không? - Câu kết luận an toàn hơn là gì?
[Đáp án tham khảo]
function createReader(value) {
return function () {
return value;
};
}Trước reader = null, global binding reader giữ reference đến returned function. Function đó cần binding value của outer environment. Sau reader = null, nếu không còn reference nào khác đến function thì reference chain này không còn từ reader. Ta không khẳng định memory được giải phóng ngay lập tức. Kết luận an toàn là function/environment có thể trở nên unreachable và việc reclaim memory thuộc Garbage Collector implementation.
9. Edge Cases (Các trường hợp ngoại lệ)
Edge Case 1 — Outer return không có nghĩa mọi environment đều phải giữ
function outer() {
const x = 10;
return function inner() {
return "hello";
};
}
const fn = outer();Phân tích
inner không cần x để tạo observable behavior. Mental model không nên biến thành "cứ return inner function thì toàn bộ outer environment bắt buộc được giữ nguyên mãi". Engine có thể tối ưu những bindings không cần thiết. Điều learner phải theo dõi là function cần resolve binding nào.
Edge Case 2 — Hai closures cùng giữ một environment
function outer() {
let value = 0;
return [
() => value,
() => {
value = value + 1;
},
];
}
const [read, increment] = outer();Phân tích
read và increment đến từ cùng một invocation và đều dùng value. Chúng cùng giữ khả năng truy cập một binding value. Việc bỏ reference đến một closure chưa đủ để kết luận environment không còn cần thiết nếu closure còn lại vẫn reachable.
Edge Case 3 — Closure tồn tại không đồng nghĩa memory leak
function makeFormatter(prefix) {
return (text) => `${prefix}: ${text}`;
}
const formatError = makeFormatter("ERROR");Phân tích
formatError giữ prefix là behavior có chủ đích. Đây là closure hoạt động đúng, không phải tự động là "memory leak". Leak là vấn đề ownership/reachability không mong muốn và sẽ được học sâu hơn ở Stage 11.
10. Debug Lab (Bài lab gỡ lỗi)
Debug Lab — Dữ liệu vẫn reachable dù setup function đã return
Một developer nghĩ rằng local data sẽ "biến mất" ngay khi registerHandler() kết thúc.
Symptom (Triệu chứng): Sau khi registerHandler() return, callback lưu trong handlers vẫn đọc được user.name.
Reproduction (Tái hiện lỗi):
const handlers = [];
function registerHandler() {
const user = { name: "Alice" };
handlers.push(function () {
return user.name;
});
}
registerHandler();
console.log(handlers[0]()); // "Alice"Evidence (Bằng chứng):
registerHandler()đã kết thúc.- Function Execution Context của
registerHandlerkhông còn trên Call Stack. handlers[0]vẫn là một function reachable từ global array.- Function đó cần resolve binding
user.
Hypothesis (Giả thuyết):
- Callback trong
handlersgiữ reference cần thiết đến outer environment củaregisterHandler. - Vì callback vẫn reachable, binding
uservẫn cần reachable.
Verification (Xác minh):
const callback = handlers[0];
console.log(callback()); // "Alice"Ngay cả khi gọi callback ở vị trí khác, nó vẫn resolve được user. Điều này phù hợp với lexical environment reference, không phụ thuộc nơi callback được gọi.
Root Cause (Nguyên nhân gốc rễ):
Không có bug trong Closure. Bug nằm ở mental model của developer:
"Function return → mọi local data bị xóa ngay."
Mental model đúng:
"Execution Context kết thúc, nhưng environment cần bởi closure còn reachable có thể tiếp tục reachable."
Fix (Sửa):
Nếu callback không còn cần thiết, code phải ngừng giữ reference đến callback theo lifecycle của hệ thống:
handlers.length = 0;Trong ví dụ toy này, thao tác trên loại bỏ references từ array tới callbacks.
Prevention (Phòng ngừa):
- Khi closure được lưu vào array/object/registry, xác định rõ ai đang giữ reference.
- Phân biệt "function đã return" với "closure đã unreachable".
- Với lifecycle dài, luôn hỏi: "Reference nào sẽ release callback này?"
- Không dùng từ "memory leak" chỉ vì thấy closure giữ data; cần phân biệt retention có chủ đích và retention ngoài ý muốn.
11. Design Exercise (Bài tập thiết kế giải pháp)
Không áp dụng Design Exercise đầy đủ ở depth L4.
Ở lesson này, yêu cầu dừng ở lifecycle reasoning:
Who creates the closure?
↓
Who keeps the closure?
↓
What outer bindings does it need?
↓
When should the holder release it?Design API private state sẽ được thực hành ở Lesson 1.4.3 và Factory Functions ở Lesson 1.4.4.
12. Production Scenario (Tình huống thực tế)
Production Scenario — Callback Registry
Một module giữ danh sách callback:
const callbacks = [];
function setupFeature(config) {
function onRun() {
return config.mode;
}
callbacks.push(onRun);
}Code gọi:
setupFeature({
mode: "production",
});setupFeature() kết thúc ngay sau đó. Tuy nhiên callbacks vẫn giữ onRun.
Context: Callback cần sử dụng config.mode ở một thời điểm sau khi setup đã hoàn tất.
Constraint: Không được giả định local bindings biến mất chỉ vì setup function đã return.
Behavior:onRun vẫn có thể đọc config vì callback còn reachable từ registry và closure vẫn giữ reference cần thiết đến outer environment.
Engineering Question: Khi feature bị disable, registry có tiếp tục giữ callback không?
Nếu lifecycle của hệ thống yêu cầu callback không còn được sử dụng, reference holder phải có cơ chế loại bỏ callback. Lesson này chưa đi vào memory profiling hay GC internals; mục tiêu là nhận ra lifetime của data bị ảnh hưởng bởi lifetime của references giữ closure.
Code Review Lens
Khi thấy callback được lưu vào collection có lifetime dài, hãy hỏi:
"Ai sở hữu callback này, và lúc nào reference đến nó được release?"
Đây là câu hỏi lifecycle, không chỉ là câu hỏi syntax Closure.
13. AI-assisted Exercise (Bài tập với AI)
Level B — Challenge (Thách thức)
- Tự trả lời trước: Viết 5 câu giải thích khác nhau giữa "Execution Context đã kết thúc" và "Outer Environment vẫn reachable".
- Hỏi AI: Dùng prompt: "When a JavaScript outer function returns but a closure still uses its variables, what exactly remains reachable? Is the old execution context still on the call stack?"
- So sánh: Kiểm tra xem AI có nói execution context "vẫn sống trên stack" hay closure "copy toàn bộ variables" không.
- Challenge: Hỏi tiếp: "If the last reachable reference to the closure is removed, can we guarantee memory is immediately freed?"
- Verify: Đối chiếu với mental model Lexical Environment + references; không yêu cầu AI giải thích GC internals.
Gợi ý
Câu trả lời đạt yêu cầu phải phân biệt được:
- Execution Context rời Call Stack.
- Closure function vẫn có thể reachable.
- Needed outer environment/bindings vẫn có thể reachable.
- Không thể suy ra thời điểm Garbage Collector reclaim memory.
Đáp án tham khảo
Bạn nghĩ:
- Khi outer function return, execution của invocation đó đã kết thúc.
- Execution Context không tiếp tục nằm trên Call Stack.
- Nếu returned closure còn reachable, closure vẫn giữ reference cần thiết đến lexical environment.
- Vì vậy captured bindings vẫn có thể được resolve ở lần gọi sau.
- Nếu closure không còn reachable và không còn reference chain khác, environment có thể trở nên unreachable.
AI có thể nói sai hoặc mơ hồ:
- "JavaScript giữ execution context lại trong memory."
- "Closure copy toàn bộ local variables."
- "Khi set closure = null thì memory được free ngay."
Điểm cần sửa:
- Không đồng nhất Execution Context với Lexical Environment.
- Không biến mental model reference thành copy.
- Không hứa về thời điểm GC.
14. Teach Back (Dạy lại)
Giải thích cho một đồng nghiệp junior trong 2 phút:
"Outer function đã return rồi. Tại sao closure vẫn truy cập được local variable, và environment đó tồn tại đến bao lâu?"
Yêu cầu:
- Dùng đúng terminology: Execution Context, Call Stack, Lexical Environment, reference, reachable.
- Không dùng câu "vì JavaScript nhớ" làm giải thích cuối cùng.
- Không nói Execution Context cũ vẫn nằm trên stack.
- Không khẳng định thời điểm Garbage Collector giải phóng memory.
Mô phỏng
- Bạn nói:
- Khi outer function được gọi, engine tạo Execution Context và lexical environment cho invocation đó.
- Inner function được tạo trong lexical environment này và có outer environment reference cần thiết để resolve các bindings bên ngoài.
- Khi outer function return, Execution Context của nó bị pop khỏi Call Stack. Outer function không còn đang execute.
- Tuy nhiên, nếu inner function được return và vẫn reachable qua một variable, object hoặc collection, function đó vẫn có thể cần outer bindings.
- Vì thế environment cần thiết vẫn reachable thông qua closure. Khi inner function được gọi sau này, Variable Resolution vẫn có thể tìm thấy các bindings đó.
- Nếu không còn reference reachable tới closure và không còn chain nào khác giữ environment, environment có thể trở nên unreachable. Nhưng JavaScript code không nên khẳng định chính xác thời điểm memory được thu hồi.
💡 Hình dung Call Stack là bàn làm việc, còn environment là hồ sơ. Outer function rời bàn làm việc, nhưng hồ sơ chưa thể cất đi nếu returned function vẫn đang giữ chìa khóa để tra cứu nó.
Gợi ý đánh giá bản thân
- Bạn có phân biệt được "không còn execute" với "không còn reachable" không?
- Bạn có vẽ được reference chain giữ environment tồn tại không?
- Bạn có giải thích được tại sao bỏ một closure chưa chắc đủ nếu closure khác vẫn giữ cùng environment không?
- Bạn có tránh được kết luận "closure = memory leak" không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá |
|---|---|
| Phân biệt execution lifetime và environment lifetime | Teach Back — phân biệt đúng Context / Environment |
| Giải thích environment retention | Worked Example — trace reference chain |
| Vẽ closure lifetime diagram | Lab 1 — diagram đúng từ holder → function → environment |
| Dự đoán closure lifetime behavior | Prediction — 3/3 đúng |
| Nhận biết shared retained environment | Edge Case 2 |
| Debug retained data | Debug Lab — tìm đúng reference holder |
| Nhận biết giới hạn của mental model | Edge Case 1 + AI Exercise — không suy diễn GC implementation |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể giải thích sự khác nhau giữa Execution Context lifetime và Lexical Environment reachability.
- [ ] Có thể giải thích tại sao outer return nhưng closure vẫn resolve captured binding.
- [ ] Có thể vẽ reference chain
holder → closure → outer environment. - [ ] Dự đoán đúng 3/3 prediction scenarios.
- [ ] Có thể giải thích tại sao hai closures trong cùng invocation có thể cùng giữ một environment.
- [ ] Có thể debug một case data vẫn reachable vì callback còn được giữ trong collection.
- [ ] Không nói "closure copy variable" như một technical fact.
- [ ] Không nói "outer execution context vẫn nằm trên stack" sau khi outer return.
- [ ] Không kết luận "closure = memory leak".
- [ ] Biết rằng khi reference chain biến mất, environment có thể trở nên unreachable nhưng thời điểm memory reclaim là implementation detail.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Lesson 1.4.1 — Closure Formation. Bạn đã học inner function giữ outer environment reference và dùng reference đó để resolve captured variables.
Current (Hiện tại): Closure và Lifetime. Bạn tách rõ Execution Context đã kết thúc khỏi environment vẫn còn reachable vì closure còn được giữ reference.
Next (Tiếp theo):
- Lesson 1.4.3 — Closure làm Private State (dùng lifetime của environment để duy trì state có chủ đích)
- Lesson 1.4.4 — Factory Functions (mỗi invocation tạo state/lifetime riêng)
- Lesson 1.4.6–1.4.7 — Callback + Async (closure tồn tại qua thời gian)
- Stage 8 — React Stale Closure (lifetime + captured state trong rendering)
- Stage 11 — Memory & Debugging (reachability, retention, profiling và leak diagnosis)