Lesson 1.4.9 — Closure & Memory
0. Metadata (Thông tin bài học)
| Field | Value |
|---|---|
| Stage | 1 |
| Module | 1.4 — Closures |
| Lesson | 1.4.9 — Closure & Memory |
| Competency | C02 — JavaScript Runtime (C02.5 Closure) |
| Depth Target | L3 (Use) |
| Prerequisites | Closure Formation (1.4.1), Closure Lifetime (1.4.2), Closure + Async (1.4.7), Stale Closure (1.4.8), Variable Resolution (1.2.6) |
| Estimated Cognitive Load | High |
Out of Scope (Ngoài phạm vi)
- Garbage Collection algorithms
- Mark-and-sweep implementation details
- Heap snapshots và memory profiling
- Weak references,
WeakMap,WeakRef,FinalizationRegistry - V8 heap generations và GC tuning
- Production memory leak diagnosis ở mức engine
- Những chủ đề trên sẽ quay lại ở Stage 11
1. Why This Exists (Vì sao cần học)
Ở các lesson trước, bạn đã dùng Closure để giữ state tồn tại sau khi outer function return.
Ví dụ:
function createReader() {
const data = {
value: 42,
};
return function read() {
return data.value;
};
}
const reader = createReader();Ta đã biết reader() vẫn đọc được data.value.
Nhưng điều đó kéo theo một câu hỏi mới:
Nếu closure vẫn cần
data, objectdatacó thể biến mất khỏi memory ngay sau khicreateReader()return không?
Không.
Ở mức mental model của Stage 1:
Closure
↓
retains reference
↓
referenced object remains reachableĐây là behavior cần thiết để Closure hoạt động.
Nhưng cùng cơ chế đó cũng có thể trở thành vấn đề nếu:
- Callback không còn cần thiết nhưng vẫn bị giữ trong registry.
- Closure vô tình giữ một object lớn.
- Một collection giữ callbacks lâu hơn lifecycle dự kiến.
- Developer nghĩ "function đã return nên local data chắc đã biến mất".
- Một reference chain dài làm object tiếp tục reachable ngoài ý muốn.
Lesson này không biến bạn thành memory expert.
Mục tiêu là hình thành câu hỏi đúng:
Reference nào đang giữ object này còn reachable?
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.
- [ ] Phân biệt Execution Context kết thúc với outer environment vẫn reachable.
- [ ] Biết callback/function object có thể được giữ trong variable, object hoặc collection.
- [ ] Trace được callback capture binding nào.
- [ ] Phân biệt object reference với object value.
- [ ] Biết stale closure có thể giữ old environment/data.
Nếu thiếu, quay lại Lesson 1.4.1–1.4.8 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 Closure có thể làm referenced object tiếp tục reachable.
- Vẽ reference chain từ holder → closure → environment → object.
- Phân biệt expected retention với memory leak ở mức conceptual.
- Dự đoán object nào còn reachable khi callback vẫn được giữ.
- Debug một case callback registry giữ data lâu hơn lifecycle dự kiến.
- Nhận biết khi bỏ reference tới closure có thể làm reference chain biến mất.
- Chuẩn bị mental model cho Memory & GC ở Stage 11.
4. Mental Model (Mô hình tư duy)
Mental Model — Closure & Reachability
Closure không "giữ memory" một cách thần bí.
Hãy trace reference chain:
Reachable Holder
↓
Function / Closure
↓
Outer Environment
↓
Captured Binding
↓
Referenced ObjectNếu chain này vẫn còn reachable, object ở cuối chain cũng có thể tiếp tục reachable.
Ví dụ:
function createReader() {
const profile = {
name: "Alice",
};
return function () {
return profile.name;
};
}
const reader = createReader();Mental model:
Global Environment
└── reader
↓
Closure
↓
Outer Environment
↓
profile binding
↓
{ name: "Alice" }profile không còn được code bên ngoài truy cập trực tiếp bằng identifier.
Nhưng object vẫn nằm trong reference chain mà reader cần.
Điểm quan trọng:
Reachable không có nghĩa đang được sử dụng ngay lúc này.
Một object có thể không được truy cập trong nhiều phút nhưng vẫn reachable vì còn một reference chain tới nó.
Retention ≠ Leak
Không nên nói:
"Closure giữ object nên đó là memory leak."
Closure giữ data có thể là behavior đúng.
Leak chỉ là một vấn đề khi data/reference còn được giữ ngoài lifecycle mà system cần.
Ở Stage 1, chỉ cần phân biệt:
expected retention
vs
unexpected retentionChưa cần chẩn đoán heap hoặc GC sâu.
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
- Reachability: Một value/object vẫn có thể được đi tới qua reference chain từ một holder còn reachable.
- Retained Reference: Reference khiến một object tiếp tục reachable.
- Closure Retention: Closure giữ outer bindings; bindings có thể giữ references tới objects.
- Expected Retention: Data còn được giữ vì callback/state vẫn cần dùng.
- Unexpected Retention: Data còn được giữ dù lifecycle logic không còn cần nó.
- Release Point: Nơi application ngừng giữ reference tới callback/closure theo lifecycle.
Supporting (Hỗ trợ)
- Array, object, registry và event system có thể giữ callback.
- Một callback có thể capture object trực tiếp hoặc qua outer binding.
- Nhiều callbacks có thể cùng giữ reference tới cùng object.
- Xóa một reference chưa chắc đủ nếu reference khác vẫn còn.
Awareness (Biết tồn tại)
- Garbage Collector quyết định khi nào memory không còn reachable được reclaim.
- Engine có thể optimize cách lưu captured bindings.
- Heap snapshot và retained-size analysis thuộc Stage 11.
- Weak references là công cụ nâng cao và chưa cần ở đây.
6. Worked Example (Ví dụ phân tích từng bước)
Worked Example — Registry giữ callback, callback giữ object:
const handlers = [];
function registerUserHandler() {
const user = {
id: 1,
name: "Alice",
};
function handler() {
return user.name;
}
handlers.push(handler);
}
registerUserHandler();Step 1 — registerUserHandler() được gọi:
Tạo environment chứa binding:
userBinding trỏ tới object:
{
id: 1,
name: "Alice",
}Step 2 — handler được tạo:
handler dùng:
user.name;nên closure giữ khả năng resolve binding user.
Step 3 — Handler được push vào array:
handlers.push(handler);Array handlers là một holder còn reachable.
Ta có chain:
handlers
↓
handler
↓
outer environment
↓
user binding
↓
user objectStep 4 — registerUserHandler() return:
Execution Context kết thúc.
Nhưng handlers vẫn giữ callback.
Callback vẫn giữ outer environment.
Outer environment giữ user.
Vì vậy user object vẫn reachable.
Step 5 — Nếu callback còn cần thiết:
Đây là expected retention.
System vẫn có lý do để giữ handler và user data.
Step 6 — Nếu feature đã bị remove nhưng handler vẫn nằm trong registry:
Lúc này retention có thể trở thành unwanted lifecycle behavior.
Step 7 — Kết luận:
Memory reasoning bắt đầu từ:
Who still holds a reference?không bắt đầu từ:
Which function already returned?7. Prediction Exercise (Bài tập dự đoán)
Đừng chạy code. Hãy vẽ reference chain trước khi trả lời.
Prediction 1
function createReader() {
const data = {
value: 10,
};
return () => data.value;
}
const reader = createReader();Sau khi createReader() return, object data còn reachable qua chain nào?
Prediction 2
function createReader() {
const data = {
value: 10,
};
return () => data.value;
}
let reader = createReader();
reader = null;Nếu không còn reference nào khác tới returned function, ta có được phép khẳng định memory được giải phóng ngay lập tức không?
Prediction 3
function createPair() {
const data = {
value: 10,
};
return {
first: () => data.value,
second: () => data.value,
};
}
const pair = createPair();
const first = pair.first;
pair.first = null;
pair.second = null;data có chắc chắn không còn reachable không?
[Đáp án & Giải thích]
Prediction 1:
- Chain:text
reader ↓ closure ↓ outer environment ↓ data binding ↓ object - Object vẫn reachable vì
readervẫn giữ closure.
- Chain:
Prediction 2:
- Không được khẳng định memory được giải phóng ngay lập tức.
- Ta chỉ có thể nói: nếu không còn reference chain nào tới closure/environment/object, chúng có thể trở nên unreachable.
- Thời điểm GC reclaim memory là implementation detail.
Prediction 3:
- Chưa chắc.
- Variable
firstvẫn giữ reference tới closurepair.firstcũ. - Closure đó vẫn cần
data. - Vì vậy
datavẫn có thể reachable qua:text first → closure → environment → data
Transfer Check — Shared Object:
const shared = {
value: 1,
};
function createReader() {
return () => shared.value;
}
const a = createReader();
const b = createReader();Hãy trả lời:
avàbcó thể cùng giữ khả năng truy cập object nào?- Bỏ
acó đủ để kết luậnsharedkhông còn reachable không? - Closure ở đây giữ local object hay global binding?
Gợi ý trả lời
- Cả hai đều resolve global binding
shared, binding này trỏ tới cùng object. - Không.
bvà global bindingsharedvẫn còn. - Closure resolve global binding
shared, không phải một local object riêng của mỗi invocation.
8. Implementation Lab (Bài lab thực hành)
Lab 1 — Guided: Vẽ reference chain
Implement:
function createSession(name) {
const session = {
name,
};
return function getName() {
return session.name;
};
}
const getName = createSession("Alice");Không cần sửa code.
Hãy vẽ:
holder
→ closure
→ environment
→ binding
→ object[Đáp án tham khảo]
getName
↓
returned function
↓
createSession environment
↓
session binding
↓
{ name: "Alice" }- Giải thích: Returned function dùng
session.name, nên object vẫn nằm trong reachable chain khigetNamecòn reachable.
Lab 2 — Partial Scaffold: Registry + Unregister
Hoàn thiện một registry nhỏ:
const handlers = [];
function register(handler) {
handlers.push(handler);
return function unregister() {
// TODO: remove đúng handler khỏi array
};
}Dùng:
function createHandler(data) {
return () => data.value;
}
const handler = createHandler({
value: 42,
});
const unregister = register(handler);
unregister();Mục tiêu:
- Hiểu
handlerslà reference holder. unregister()phải loại bỏ reference của registry tới handler.- Không cần đo memory.
[Đáp án tham khảo]
const handlers = [];
function register(handler) {
handlers.push(handler);
return function unregister() {
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
}
};
}- Giải thích: Registry không còn giữ handler sau
unregister(). Tuy nhiên nếu variablehandlerở nơi khác vẫn còn reference tới function, closure vẫn reachable qua reference đó.
Lab 3 — Independent: Release lifecycle đúng chỗ
Cho:
const subscriptions = [];
function subscribe(user) {
const callback = () => {
return user.name;
};
subscriptions.push(callback);
// TODO: return cleanup function
}Implement cleanup:
const cleanup = subscribe({
name: "Alice",
});
cleanup();Constraints:
- Cleanup loại bỏ đúng callback khỏi
subscriptions. - Không xóa toàn bộ array.
- Giải thích reference nào bị loại bỏ.
- Không dùng WeakMap/WeakRef.
[Một phương án tham khảo]
const subscriptions = [];
function subscribe(user) {
const callback = () => {
return user.name;
};
subscriptions.push(callback);
return function cleanup() {
const index = subscriptions.indexOf(callback);
if (index !== -1) {
subscriptions.splice(index, 1);
}
};
}- Giải thích:
subscriptionskhông còn giữ callback sau cleanup. Nếu callback không còn được giữ ở nơi khác, reference chain từ registry đếnusercó thể biến mất.
9. Edge Cases (Các trường hợp ngoại lệ)
Edge Case 1 — Function return không quyết định memory lifetime
function createReader() {
const largeObject = {
value: 1,
};
return () => largeObject.value;
}
const reader = createReader();Phân tích
createReader() return chỉ cho biết execution của function đã kết thúc.
Nó không đủ để kết luận largeObject không còn reachable.
Phải trace reader → closure → environment → largeObject.
Edge Case 2 — Xóa một reference nhưng còn reference khác
const callbacks = [];
function createHandler(data) {
return () => data.value;
}
let handler = createHandler({
value: 1,
});
callbacks.push(handler);
handler = null;Phân tích
Gán handler = null chỉ loại bỏ một reference.
Array callbacks vẫn giữ function.
Do đó closure vẫn reachable qua callbacks[0], và captured data vẫn có thể reachable.
Edge Case 3 — Closure không capture object thì không cần giữ object vì Closure đó
function createHandler() {
const data = {
value: 1,
};
return function handler() {
return "ok";
};
}Phân tích
handler không dùng data.
Không nên kết luận chỉ vì function được tạo bên trong createHandler thì toàn bộ mọi local object bắt buộc phải được giữ nguyên vì Closure.
Mental model cần bám vào:
Callback thực sự cần resolve binding nào?
Engine optimization cụ thể vẫn ngoài scope.
10. Debug Lab (Bài lab gỡ lỗi)
Debug Lab — Callback registry giữ data sau khi feature đã đóng
Symptom (Triệu chứng): Feature đã được "close", nhưng callback cũ vẫn còn trong registry và vẫn truy cập được user data.
Reproduction (Tái hiện lỗi):
const handlers = [];
function openFeature(user) {
function onAction() {
return user.name;
}
handlers.push(onAction);
}
openFeature({
name: "Alice",
});
console.log(handlers.length); // 1Developer chỉ "đóng UI" nhưng không remove callback khỏi handlers.
Evidence (Bằng chứng):
- Global
handlersvẫn reachable. handlers[0]vẫn là callback.- Callback dùng parameter binding
user. usertrỏ tới object{ name: "Alice" }.- Không có cleanup nào remove callback khỏi registry.
Hypothesis (Giả thuyết):
Registry giữ callback lâu hơn lifecycle của feature.
Callback tiếp tục giữ outer environment và user object reachable.
Verification (Xác minh):
Thêm cleanup thủ công:
handlers.length = 0;Reference từ registry tới callback biến mất.
Ở mức conceptual, nếu không còn holder khác, callback/environment/user có thể trở nên unreachable.
Root Cause (Nguyên nhân gốc rễ):
Không phải Closure "leak memory" tự động.
Root cause là:
long-lived registry
+
missing lifecycle cleanup
+
callback captures userFix (Sửa):
Thiết kế registration trả cleanup function:
function openFeature(user) {
function onAction() {
return user.name;
}
handlers.push(onAction);
return function closeFeature() {
const index = handlers.indexOf(onAction);
if (index !== -1) {
handlers.splice(index, 1);
}
};
}Prevention (Phòng ngừa):
- Với registry/subscription/event system, luôn xác định cleanup lifecycle.
- Trace holder nào đang giữ callback.
- Không kết luận function return đồng nghĩa local object đã hết lifetime.
- Không gọi mọi retention là leak.
- Khi memory topic xuất hiện, bắt đầu bằng reference graph trước khi đoán GC behavior.
11. Design Exercise (Bài tập thiết kế giải pháp)
Không áp dụng Design Exercise đầy đủ ở depth L3.
Thay vào đó, làm một lifecycle decision.
Bạn có API:
subscribe(callback);Callback có thể capture object lớn.
Hai options:
Option A
subscribe(callback);
// không có cách unsubscribeOption B
const unsubscribe = subscribe(callback);
unsubscribe();Nếu callback lifecycle có điểm kết thúc rõ ràng, Option B cung cấp release point tốt hơn.
Hãy trả lời:
Who owns callback?
Who keeps callback?
When is callback no longer needed?
Which API releases that reference?Engineering Decision
Memory-safe design thường bắt đầu từ ownership + lifecycle, không bắt đầu từ GC tricks.
12. Production Scenario (Tình huống thực tế)
Production Scenario — Event Subscription
Một dashboard subscribe callback:
const listeners = [];
function subscribe(listener) {
listeners.push(listener);
}
function mountPanel(user) {
const panelData = {
user,
cache: new Array(1000).fill("data"),
};
subscribe(() => {
return panelData.user.name;
});
}Context: Panel có lifecycle mount → unmount.
Constraint: Listener registry sống lâu hơn panel.
Risk: Nếu unmount không remove listener:
listeners
→ callback
→ panelData
→ user + cacheReference chain vẫn tồn tại.
Important: Lesson này không khẳng định chính xác memory size hay thời điểm GC.
Chỉ cần nhận ra:
Long-lived holder có thể kéo dài lifetime của data mà callback capture.
Production question:
Khi panel unmount, callback được remove ở đâu?
Đó là câu hỏi lifecycle quan trọng hơn câu:
"Closure có leak không?"
13. AI-assisted Exercise (Bài tập với AI)
Level B — Challenge (Thách thức)
- Tự trả lời trước: Closure có tự động gây memory leak không?
- Hỏi AI: "Explain how a JavaScript closure can retain object references without claiming every closure is a memory leak."
- Inspect: AI có nói "outer function returns, variables stay forever" không?
- Challenge: Đưa case có callback registry và yêu cầu AI vẽ reference chain.
- Verify: Kiểm tra AI có phân biệt expected retention, unexpected retention và GC timing không.
Gợi ý
Câu trả lời đủ tốt phải đi theo:
reachable holder
→ closure
→ environment
→ captured binding
→ objectvà phải tránh:
closure exists
→ memory leakĐáp án tham khảo
- Closure có thể giữ outer references reachable.
- Nếu outer binding trỏ tới object, object có thể tiếp tục reachable.
- Đây là behavior bình thường khi callback vẫn cần object.
- Vấn đề chỉ xuất hiện khi reference bị giữ lâu hơn lifecycle cần thiết.
- Khi reference chain biến mất, object có thể trở nên unreachable.
- Không được khẳng định chính xác khi memory được reclaim nếu chưa đi vào GC internals.
14. Teach Back (Dạy lại)
Giải thích cho một đồng nghiệp junior trong 2 phút:
"Closure có thể ảnh hưởng lifetime của object như thế nào, và tại sao closure không đồng nghĩa với memory leak?"
Yêu cầu:
- Dùng đúng terminology: reference, reachable, closure, environment, holder.
- Vẽ được một reference chain.
- Phân biệt expected retention với unwanted retention.
- Không giải thích bằng GC algorithm.
- Không hứa memory được free ngay sau khi set variable thành
null.
Mô phỏng
- Bạn nói:
- Closure có thể giữ reference cần thiết tới outer environment.
- Nếu một binding trong environment trỏ tới object, và closure vẫn reachable, object đó cũng có thể tiếp tục reachable qua reference chain.
- Ví dụ một global array giữ callback; callback dùng
user; lúc đó chain là array → callback → environment → user binding → user object. - Đây không tự động là leak. Nếu callback vẫn cần dùng, retention là expected behavior.
- Nó trở thành lifecycle problem khi feature đã kết thúc nhưng holder như registry vẫn giữ callback ngoài ý muốn.
- Cleanup thường là loại bỏ reference ở đúng owner/lifecycle boundary.
- Nếu không còn reference chain, object có thể trở nên unreachable, nhưng thời điểm Garbage Collector reclaim memory là implementation detail.
💡 Hình dung object như một kiện hàng. Chỉ cần còn một sợi dây reference nối từ nơi đang reachable tới kiện hàng, ta chưa thể coi nó đã bị bỏ đi.
Gợi ý đánh giá bản thân
- Bạn có tìm được holder đầu tiên trong reference chain không?
- Bạn có chỉ ra closure capture binding nào không?
- Bạn có phân biệt function return với object unreachable không?
- Bạn có tránh gọi mọi Closure retention là leak không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá |
|---|---|
| Giải thích Closure retention | Teach Back — holder → closure → environment → object |
| Vẽ reference chain | Worked Example + Lab 1 |
| Phân biệt expected vs unwanted retention | Edge Cases + Production Scenario |
| Dự đoán reachability | Prediction — 3/3 đúng |
| Implement cleanup lifecycle | Lab 2–3 |
| Debug registry retention | Debug Lab — xác định đúng long-lived holder |
| Chuẩn bị cho Stage 11 | Spiral Connection |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể vẽ reference chain từ holder tới captured object.
- [ ] Có thể giải thích vì sao object vẫn reachable sau outer function return.
- [ ] Dự đoán đúng 3/3 prediction scenarios.
- [ ] Có thể phân biệt expected retention và unwanted retention.
- [ ] Không gọi mọi closure là memory leak.
- [ ] Có thể chỉ ra long-lived holder đang giữ callback nào.
- [ ] Implement được cleanup function remove callback khỏi registry.
- [ ] Biết xóa một reference chưa chắc đủ nếu reference khác vẫn tồn tại.
- [ ] Không khẳng định memory được reclaim ngay khi reference biến mất.
- [ ] Sẵn sàng học reachability, GC và memory profiling sâu hơn ở Stage 11.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Closure Formation → Lifetime → Private State → Factory Functions → Loop → Callback → Async → Stale Closure. Bạn đã biết closure giữ environment và có thể sống lâu hơn creator function.
Current (Hiện tại): Closure & Memory. Bạn dùng reference chain để thấy captured objects có thể tiếp tục reachable khi closure còn được giữ.
Next (Tiếp theo):
- Module 1.5 — Call Stack & Execution Tracing (gộp Execution Context + Scope + Closure thành kỹ năng trace hoàn chỉnh)
- Stage 3 — Async lifecycle và cleanup
- Stage 8 — React effects, callbacks và component lifecycle
- Stage 11 — Garbage Collection, heap, retained references và memory profiling
- Stage 14 — Production memory debugging và engineering judgment