Lesson 1.4.3 — Closure làm Private State
0. Metadata (Thông tin bài học)
| Field | Value |
|---|---|
| Stage | 1 |
| Module | 1.4 — Closures |
| Lesson | 1.4.3 — Closure làm Private State |
| Competency | C02 — JavaScript Runtime (C02.5 Closure) |
| Depth Target | L5 (Implement) |
| Prerequisites | Closure Formation (1.4.1), Closure và Lifetime (1.4.2), Lexical Environment (1.2.7), Variable Resolution (1.2.6) |
| Estimated Cognitive Load | High |
Out of Scope (Ngoài phạm vi)
- Factory patterns với nhiều loại instance/API (Lesson 1.4.4)
- Constructor function,
new, prototype vàclass(Stage 2) - JavaScript private class fields
#field(Stage 2) - Module-level encapsulation và package boundaries (Stage 7/12)
- Async state, callback lifecycle và race conditions (Stage 3)
- React state / stale closure (Stage 8)
- Garbage Collection internals và memory profiling (Stage 11)
1. Why This Exists (Vì sao cần học)
Bạn cần một counter có state:
let count = 0;
function increment() {
count = count + 1;
}Code chạy được, nhưng count đang nằm ở scope bên ngoài. Bất kỳ code nào có quyền truy cập binding đó đều có thể thay đổi nó:
count = 999;Nếu requirement là:
Chỉ cho phép thay đổi state thông qua những operation đã định nghĩa.
thì việc expose trực tiếp count làm mất quyền kiểm soát state.
Closure cho phép ta thay đổi cách thiết kế:
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getValue()); // 2Caller biết cách sử dụng state, nhưng không có identifier count để sửa trực tiếp.
Đây là bước chuyển quan trọng:
state
+
operations
+
controlled accessNếu không hiểu private state bằng Closure, bạn sẽ dễ:
- Đặt mutable state ở global scope.
- Expose state rồi hy vọng caller "đừng sửa".
- Nhầm private binding với immutable value.
- Return một snapshot rồi tưởng đó là live state.
- Không biết vì sao nhiều methods có thể cùng đọc/ghi một binding.
- Thiết kế API theo data structure thay vì theo invariant cần bảo vệ.
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.
- [ ] Biết outer function return nhưng environment vẫn có thể reachable qua closure.
- [ ] Phân biệt Execution Context với Lexical Environment.
- [ ] Biết nhiều inner functions trong cùng invocation có thể cùng resolve một outer binding.
- [ ] Phân biệt reassignment của binding với mutation của object.
- [ ] Trace được Variable Resolution từ method/function ra outer environment.
Nếu thiếu, quay lại Lesson 1.4.1–1.4.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 private state bằng environment + closure, không bằng câu "function nhớ biến".
- Implement
createCounter()vớiincrement(),decrement()vàgetValue(). - Chứng minh
countkhông được expose trực tiếp qua public API. - Dự đoán behavior khi nhiều methods cùng đọc/ghi một captured binding.
- Phân biệt private binding với immutable state và với object reference được expose.
- Debug API có state bên trong nhưng public property lại bị stale hoặc bypass invariant.
- Thiết kế một API nhỏ sao cho mutation đi qua operations được kiểm soát.
4. Mental Model (Mô hình tư duy)
Mental Model — Private State bằng Closure
Private state trong lesson này được tạo bằng cách:
- Đặt state trong lexical environment của outer function.
- Không return trực tiếp binding/state đó như một public property để caller sửa tùy ý.
- Return các functions/methods được phép đọc hoặc thay đổi state.
- Các functions đó giữ reference cần thiết đến outer environment.
Caller giữ API, còn state nằm trong environment được API closures truy cập.
Ví dụ:
function createCounter() {
let count = 0;
return {
increment() {
count = count + 1;
},
decrement() {
count = count - 1;
},
getValue() {
return count;
},
};
}Mental model:
createCounter()
↓
Environment
└── count = 0
↑
├── increment()
├── decrement()
└── getValue()
Caller
└── counter
├── increment
├── decrement
└── getValueCaller có reference đến ba methods, nhưng không có binding public:
counter.count;trừ khi chính ta chủ động expose property đó.
Điểm quan trọng:
Closure không biến
countthành "private keyword". Privacy ở đây đến từ lexical scope + API boundary: caller không có đường truy cập trực tiếp tới bindingcount, chỉ có các closures mà ta trả ra.
Private ≠ Immutable
count private không có nghĩa count không thể thay đổi.
Ngược lại, increment() và decrement() được thiết kế để thay đổi cùng một binding count.
Private ở đây nghĩa là quyền truy cập trực tiếp bị giới hạn bởi lexical scope, không phải state bị đóng băng.
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
- Private Binding: Binding nằm trong lexical scope mà caller không thể resolve trực tiếp từ bên ngoài.
- Public API: Những functions/methods được return để caller tương tác với state.
- Controlled Mutation: State chỉ được thay đổi qua operations mà API cho phép.
- Shared Captured State: Nhiều methods trong cùng invocation có thể cùng đọc/ghi một binding.
- Invariant: Điều kiện mà API phải luôn bảo vệ, ví dụ counter không được nhỏ hơn
0.
Supporting (Hỗ trợ)
- Closure lifetime giúp private state tiếp tục tồn tại sau khi creator function return.
- Getter method có thể cung cấp read access mà không expose binding để caller reassign trực tiếp.
- Public API nhỏ giúp giảm số đường có thể mutate state.
- Encapsulation trong lesson này là một design effect của lexical scope + closure.
Awareness (Biết tồn tại)
- JavaScript có nhiều cách khác để tổ chức encapsulation/private state.
- Class private fields, module boundaries và WeakMap có trade-off khác.
- Object reference có thể làm rò rỉ khả năng mutation nếu API trả trực tiếp object mutable.
6. Worked Example (Ví dụ phân tích từng bước)
Worked Example — createCounter() với private state:
function createCounter() {
let count = 0;
return {
increment() {
count = count + 1;
},
decrement() {
count = count - 1;
},
getValue() {
return count;
},
};
}
const counter = createCounter();
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getValue()); // 1Step 1 — createCounter() được gọi:
- Tạo một invocation mới.
- Lexical environment của invocation chứa binding
count → 0. - Ba methods được tạo trong environment này.
Step 2 — Public API được return:
Caller nhận object:
increment
decrement
getValueNhưng object không có public property count.
Step 3 — counter.increment() được gọi:
- Method cần resolve
count. - Trong environment của chính method không có
count. - Đi theo outer environment reference.
- Tìm thấy binding
countcủacreateCounter. - Update
count: 0 → 1.
Step 4 — Gọi tiếp increment() và decrement():
Cả ba methods đều dùng cùng một binding:
count
0 → 1 → 2 → 1Không có ba bản copy count khác nhau.
Step 5 — getValue() đọc state:
counter.getValue(); // 1getValue() không cần expose binding. Nó chỉ return giá trị hiện tại của binding đó.
Step 6 — Kết luận:
Caller
↓
Public API
↓
Closures
↓
Private binding: countState được duy trì nhờ Closure Lifetime, còn quyền truy cập được kiểm soát bởi lexical scope và public API.
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 binding + closure + API boundary.
Prediction 1
function createCounter() {
let count = 0;
return {
increment() {
count = count + 1;
},
getValue() {
return count;
},
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getValue());
console.log(counter.count);Prediction 2
function createCounter() {
let count = 0;
return {
increment() {
count = count + 1;
},
reset() {
count = 0;
},
getValue() {
return count;
},
};
}
const counter = createCounter();
counter.increment();
counter.increment();
counter.reset();
console.log(counter.getValue());Prediction 3
function createState() {
let state = { score: 0 };
return {
getState() {
return state;
},
};
}
const store = createState();
const exposed = store.getState();
exposed.score = 100;
console.log(store.getState().score);[Đáp án & Giải thích]
Prediction 1:
- Output:
2rồiundefined. countnằm trong lexical environment củacreateCounter. Returned object không có propertycount, nêncounter.countlàundefined.getValue()vẫn đọc được binding vì method là closure.
- Output:
Prediction 2:
- Output:
0. increment,reset,getValuecùng truy cập một bindingcount.reset()reassign binding đó về0.
- Output:
Prediction 3:
- Output:
100. - Binding
statelà private, nhưnggetState()return chính object reference đang nằm trong binding. Caller không reassign được bindingstate, nhưng vẫn có thể mutate object mà binding trỏ tới. Private binding không tự động làm object immutable.
- Output:
Transfer Check — không cần biết pattern trước:
function createTemperature(initialValue) {
let temperature = initialValue;
return {
increase() {
temperature = temperature + 1;
},
read() {
return temperature;
},
};
}
const room = createTemperature(25);Hãy trả lời:
- Binding nào là private state?
- Caller có thể viết
room.temperature = 100để thay đổi binding private không? increase()vàread()có cùng truy cập một binding không?- Vì sao state vẫn tồn tại sau khi
createTemperature()return?
Gợi ý trả lời
temperaturelà private binding nằm trong environment của invocationcreateTemperature.- Không. Gán
room.temperature = 100chỉ tạo/thay đổi property trên returned object; nó không phải bindingtemperaturemà closures đang resolve. - Có. Cả hai methods được tạo trong cùng invocation và cùng resolve
temperature. - Returned methods vẫn reachable và cần outer binding đó, nên environment cần thiết vẫn reachable theo mental model từ Lesson 1.4.2.
8. Implementation Lab (Bài lab thực hành)
Lab 1 — Guided: createCounter()
Bước 0 — Vẽ API boundary trước khi code.
Private
└── count
Public
├── increment()
├── decrement()
└── getValue()Sau đó implement:
function createCounter() {
// TODO
}
const counter = createCounter();
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getValue()); // 1
console.log(counter.count); // undefinedConstraints:
countbắt đầu từ0.- Không dùng global variable.
- Không lưu
countbằng public property. - Chỉ
increment()vàdecrement()được thay đổicount. getValue()chỉ đọc state.
[Đáp án tham khảo]
function createCounter() {
let count = 0;
return {
increment() {
count = count + 1;
},
decrement() {
count = count - 1;
},
getValue() {
return count;
},
};
}- Giải thích:
countchỉ tồn tại trong lexical environment củacreateCounter. Ba methods là closures cùng truy cập binding đó. Caller chỉ nhận API object, không nhận bindingcount.
Lab 2 — Partial Scaffold: Counter có invariant
Requirement mới:
Counter không được nhỏ hơn
0.
Hoàn thiện:
function createCounter() {
let count = 0;
return {
increment() {
// TODO
},
decrement() {
// TODO: không cho count xuống dưới 0
},
getValue() {
// TODO
},
};
}Expected behavior:
const counter = createCounter();
counter.decrement();
console.log(counter.getValue()); // 0
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getValue()); // 1[Đáp án tham khảo]
function createCounter() {
let count = 0;
return {
increment() {
count = count + 1;
},
decrement() {
if (count > 0) {
count = count - 1;
}
},
getValue() {
return count;
},
};
}- Giải thích: Vì caller không thể sửa
counttrực tiếp, API có thể bảo vệ invariantcount >= 0tại mọi mutation point.
Lab 3 — Independent: Private Score
Implement:
const score = createScore(10);
score.add(5);
score.subtract(3);
console.log(score.getValue()); // 12Contract:
add(points)
subtract(points)
getValue()Constraints:
- Initial score được truyền vào creator function.
- Score không được âm.
pointsphải lớn hơn0; input không hợp lệ thì không thay đổi state.- State không được expose bằng property public.
- Không dùng
class.
Sau khi code, trả lời:
- Binding nào là state owner?
- Có bao nhiêu mutation points?
- Invariant được kiểm tra ở đâu?
- Vì sao
getValue()an toàn hơn việc exposescoređể caller tự sửa?
[Một phương án tham khảo]
function createScore(initialScore) {
let score = initialScore;
return {
add(points) {
if (points <= 0) return false;
score = score + points;
return true;
},
subtract(points) {
if (points <= 0 || points > score) return false;
score = score - points;
return true;
},
getValue() {
return score;
},
};
}scorelà private binding của creator invocation.addvàsubtractlà hai mutation points.- Validation nằm tại mutation boundary.
- Caller không thể bypass rule bằng cách viết trực tiếp
score = -100.
9. Edge Cases (Các trường hợp ngoại lệ)
Edge Case 1 — Public property chỉ là snapshot
function createCounter() {
let count = 0;
return {
count,
increment() {
count = count + 1;
},
getValue() {
return count;
},
};
}
const counter = createCounter();
counter.increment();
console.log(counter.count);
console.log(counter.getValue());Phân tích
Output là 0 rồi 1.
Property count trong returned object nhận giá trị tại thời điểm object được tạo. Nó không phải alias tự động tới lexical binding count. Method increment() update captured binding, còn counter.count vẫn giữ primitive value ban đầu.
Đây là bug rất dễ gặp khi cố expose "state để tiện debug".
Edge Case 2 — Getter trả object reference
function createSettings() {
const settings = {
theme: "dark",
};
return {
getSettings() {
return settings;
},
};
}Phân tích
Binding settings private, nhưng caller nhận chính object reference qua getSettings(). Caller có thể viết getSettings().theme = "light".
Nếu invariant yêu cầu caller không được mutate object trực tiếp, API cần thiết kế boundary khác. Lesson này không đi sâu vào immutable data architecture; mục tiêu là phân biệt private binding với private mutable object.
Edge Case 3 — Public method có thể phá invariant
function createCounter() {
let count = 0;
return {
setValue(nextValue) {
count = nextValue;
},
getValue() {
return count;
},
};
}Phân tích
State vẫn private về lexical access, nhưng API lại cho phép caller đặt bất kỳ value nào. Encapsulation không tự động tạo invariant tốt. Public API phải được thiết kế theo những mutation mà system thực sự muốn cho phép.
10. Debug Lab (Bài lab gỡ lỗi)
Debug Lab — Public count và private count lệch nhau
Symptom (Triệu chứng): Developer mong đợi counter.count và counter.getValue() luôn giống nhau, nhưng sau increment() chúng trả kết quả khác nhau.
Reproduction (Tái hiện lỗi):
function createCounter() {
let count = 0;
return {
count,
increment() {
count = count + 1;
},
getValue() {
return count;
},
};
}
const counter = createCounter();
counter.increment();
console.log(counter.count); // 0
console.log(counter.getValue()); // 1Evidence (Bằng chứng):
- Có lexical binding
countbên trongcreateCounter. - Returned object cũng có property tên
count. increment()reassign lexical binding.- Không có code nào update
counter.count.
Hypothesis (Giả thuyết):
- Property
countvà lexical bindingcountlà hai storage locations khác nhau. - Object property nhận value
0khi object được tạo. - Closure methods tiếp tục đọc/ghi lexical binding.
Verification (Xác minh):
counter.count = 999;
console.log(counter.count); // 999
console.log(counter.getValue()); // 1Nếu hai giá trị tiếp tục độc lập, giả thuyết được xác nhận.
Root Cause (Nguyên nhân gốc rễ):
Developer tưởng:
object.count
=
alias tới private binding countNhưng thực tế:
private binding count
≠
public property counter.countFix (Sửa):
Không expose snapshot property nếu source of truth phải là private binding:
function createCounter() {
let count = 0;
return {
increment() {
count = count + 1;
},
decrement() {
count = count - 1;
},
getValue() {
return count;
},
};
}Prevention (Phòng ngừa):
- Xác định một source of truth duy nhất cho state.
- Không duplicate private state thành public primitive property nếu không có synchronization strategy.
- Khi expose read access, ưu tiên method/getter có contract rõ hơn snapshot dễ stale.
- Trong code review, hỏi: "Property này là source of truth hay chỉ là copy?"
11. Design Exercise (Bài tập thiết kế giải pháp)
Design Exercise — Private State hay Public State?
Bạn cần thiết kế một createVolumeController().
Requirement:
increase()
decrease()
getVolume()Constraints:
- Volume bắt đầu ở
50. - Volume luôn nằm trong khoảng
0..100. - Caller không được bypass giới hạn bằng cách gán trực tiếp.
- API phải đủ nhỏ để dễ reasoning.
- Chưa dùng
class, prototype hoặc library.
Có ba options:
Option A — Public property
const controller = {
volume: 50,
};Option B — Private binding + unrestricted setter
setVolume(value)
getVolume()Option C — Private binding + domain operations
increase()
decrease()
getVolume()Hãy quyết định dựa trên:
Context
→ Constraints
→ Options
→ Decision
→ Trade-offs[Một quyết định tham khảo]
Context: Volume phải luôn nằm trong range 0..100.
Constraints: Caller không được bypass invariant bằng direct assignment.
Options:
- A đơn giản nhưng state hoàn toàn public.
- B private về lexical access, nhưng unrestricted setter vẫn cho phép API nhận invalid value nếu validation không chặt.
- C giới hạn mutation thành đúng operations mà domain cần.
Decision: Chọn Option C.
function createVolumeController() {
let volume = 50;
return {
increase() {
if (volume < 100) {
volume = volume + 1;
}
},
decrease() {
if (volume > 0) {
volume = volume - 1;
}
},
getVolume() {
return volume;
},
};
}Trade-off: API ít flexible hơn setVolume(value), nhưng đổi lại invariant dễ bảo vệ và behavior dễ reasoning hơn. Nếu requirement sau này cần set trực tiếp, có thể thêm operation có validation rõ ràng thay vì expose binding.
12. Production Scenario (Tình huống thực tế)
Production Scenario — Retry Budget của một operation
Một module có rule:
Một operation chỉ được retry tối đa 3 lần.
Implementation yếu:
const retryState = {
attempts: 0,
};Bất kỳ code nào giữ reference đều có thể:
retryState.attempts = -100;
retryState.attempts = 999;Ta muốn public API chỉ cho phép:
recordAttempt()
canRetry()
getAttempts()Một closure-based implementation đơn giản:
function createRetryBudget(maxAttempts) {
let attempts = 0;
return {
recordAttempt() {
if (attempts < maxAttempts) {
attempts = attempts + 1;
}
},
canRetry() {
return attempts < maxAttempts;
},
getAttempts() {
return attempts;
},
};
}Context: Nhiều nơi trong code cần kiểm tra retry budget.
Constraint: Caller không được tự ý sửa counter thành một value phá rule.
Decision: Giữ attempts trong lexical environment và chỉ expose operations có semantic rõ ràng.
Production value: Private state không chỉ để "giấu biến". Nó giúp giảm số đường có thể đưa state vào trạng thái invalid.
Code Review Lens
Khi review mutable state, đừng chỉ hỏi:
"Biến này có private không?"
Hãy hỏi thêm:
"Những operation nào được phép thay đổi nó, và API hiện tại có cho caller bypass invariant không?"
13. AI-assisted Exercise (Bài tập với AI)
Level C — Delegate & Inspect (Ủy quyền và kiểm tra)
- Tự thiết kế trước: Viết
createCounter()có invariantcount >= 0. - Hỏi AI: Yêu cầu AI tạo 3 implementation:
- public property;
- closure private state;
- closure private state +
setValue.
- Inspect: Với từng implementation, xác định:
- state nằm ở đâu;
- mutation points là gì;
- caller có bypass invariant không;
- có duplicate source of truth không.
- Challenge: Nếu AI nói "closure làm state immutable" hoặc "private state luôn an toàn", yêu cầu AI đưa counterexample.
- Verify: Tự viết test cố phá invariant.
Gợi ý
Mục tiêu không phải chọn solution "trông hiện đại nhất".
Bạn phải đánh giá bằng mental model:
Who owns state?
Who can mutate it?
Through which API?
Which invariant is protected?Đáp án tham khảo
- Public property: dễ đọc/ghi nhưng caller có thể bypass invariant trực tiếp.
- Closure private state: giới hạn direct access; mutation đi qua methods được return.
- Closure + unrestricted
setValue: binding vẫn private nhưng API có thể cho phép invalid state nếu setter không validate. - Kết luận: Closure là cơ chế giúp tạo boundary. Chất lượng invariant vẫn phụ thuộc cách bạn thiết kế public API.
14. Teach Back (Dạy lại)
Giải thích cho một đồng nghiệp junior trong 2 phút:
"Closure tạo private state như thế nào, và tại sao private state không tự động đồng nghĩa với immutable hoặc safe state?"
Yêu cầu:
- Dùng đúng terminology: lexical environment, binding, closure, public API, invariant.
- Giải thích được vì sao
counter.countkhông truy cập được private binding nếu property không được expose. - Giải thích được vì sao nhiều methods vẫn cùng mutate một binding.
- Đưa ít nhất một counterexample chứng minh private binding chưa đủ để bảo vệ mọi invariant.
Mô phỏng
- Bạn nói:
- Khi
createCounter()chạy, nó tạo một lexical environment có bindingcount. - Các methods
increment,decrement,getValueđược tạo trong environment đó và giữ reference cần thiết để resolvecount. - Outer function return object chứa methods, nhưng không return một public property trỏ tới binding
count. Vì vậy caller chỉ có API methods, không có identifier trực tiếp để reassign private binding. incrementvàdecrementvẫn có thể mutatecountvì private không có nghĩa immutable. Chúng là những mutation points mà API chủ động cho phép.- Nếu API return chính một mutable object reference hoặc expose một unrestricted setter, caller vẫn có thể làm state invalid. Closure chỉ giúp tạo access boundary; invariant còn phụ thuộc design của operations.
- Khi
💡 Hình dung state nằm trong một phòng khóa. Closure methods là những cửa có kiểm soát. Khóa cửa chính không có nghĩa mọi cửa phụ đều an toàn — ta vẫn phải thiết kế từng cửa cho đúng rule.
Gợi ý đánh giá bản thân
- Bạn có chỉ ra được binding nào thật sự private không?
- Bạn có phân biệt private binding với public object property cùng tên không?
- Bạn có giải thích được tại sao getter trả object reference có thể làm mất mutation control không?
- Bạn có xác định được invariant và tất cả mutation points của API không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá |
|---|---|
| Giải thích private state bằng Closure | Teach Back — environment → closure → API boundary |
Implement createCounter() | Lab 1 — đúng increment, decrement, getValue |
| Bảo vệ invariant | Lab 2 + Design Exercise |
| Dự đoán captured state behavior | Prediction — 3/3 đúng |
| Phân biệt private binding vs mutable object | Prediction 3 + Edge Case 2 |
| Debug duplicate/stale public state | Debug Lab — xác định đúng hai storage locations |
| Thiết kế mutation boundary | Design Exercise — Context → Constraints → Options → Decision |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể giải thích private state bằng lexical environment và closure.
- [ ] Implement được
createCounter()vớiincrement(),decrement(),getValue(). - [ ]
countkhông được expose trực tiếp bằng public property. - [ ] Dự đoán đúng 3/3 prediction scenarios.
- [ ] Có thể chứng minh nhiều methods trong cùng API đang dùng cùng một captured binding.
- [ ] Có thể phân biệt private binding với immutable state.
- [ ] Có thể giải thích tại sao return object reference có thể cho caller mutate state gián tiếp.
- [ ] Debug được bug public snapshot lệch khỏi private source of truth.
- [ ] Có thể xác định tất cả mutation points và invariant của một API nhỏ.
- [ ] Có thể thiết kế private-state API không cho caller bypass một invariant đơn giản.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Lesson 1.4.1 — Closure Formation và Lesson 1.4.2 — Closure Lifetime. Bạn đã biết function giữ outer environment reference và environment có thể tiếp tục reachable sau khi creator function return.
Current (Hiện tại): Closure làm Private State. Bạn dùng chính cơ chế đó để giữ mutable state trong lexical environment và chỉ expose những operations được phép truy cập state.
Next (Tiếp theo):
- Lesson 1.4.4 — Factory Functions (tạo nhiều instance, mỗi instance có state riêng)
- Lesson 1.4.5 — Closure trong Loop (binding nào được capture?)
- Lesson 1.4.6–1.4.7 — Callback + Async (private/captured state sống qua thời gian)
- Stage 8 — React state và stale closure
- Stage 11 — Memory retention và debugging