Skip to content

Lesson 1.4.9 — Closure & Memory ​

Bài 1.4.9 — Closure & Memory
Reachability, Retained References & Memory Lifecycle • 28 phút
0:00 / 0:00

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

FieldValue
Stage1
Module1.4 — Closures
Lesson1.4.9 — Closure & Memory
CompetencyC02 — JavaScript Runtime (C02.5 Closure)
Depth TargetL3 (Use)
PrerequisitesClosure 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 LoadHigh

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ụ:

js
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, object data có thể biến mất khỏi memory ngay sau khi createReader() return không?

Không.

Ở mức mental model của Stage 1:

text
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ể:

  1. Giải thích tại sao Closure có thể làm referenced object tiếp tục reachable.
  2. Vẽ reference chain từ holder → closure → environment → object.
  3. Phân biệt expected retention với memory leak ở mức conceptual.
  4. Dự đoán object nào còn reachable khi callback vẫn được giữ.
  5. Debug một case callback registry giữ data lâu hơn lifecycle dự kiến.
  6. Nhận biết khi bỏ reference tới closure có thể làm reference chain biến mất.
  7. 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:

text
Reachable Holder
      ↓
Function / Closure
      ↓
Outer Environment
      ↓
Captured Binding
      ↓
Referenced Object

Nếu chain này vẫn còn reachable, object ở cuối chain cũng có thể tiếp tục reachable.

Ví dụ:

js
function createReader() {
  const profile = {
    name: "Alice",
  };

  return function () {
    return profile.name;
  };
}

const reader = createReader();

Mental model:

text
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:

text
expected retention
vs
unexpected retention

Chư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:

js
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:

text
user

Binding trỏ tới object:

js
{
  id: 1,
  name: "Alice",
}

Step 2 — handler được tạo:

handler dùng:

js
user.name;

nên closure giữ khả năng resolve binding user.

Step 3 — Handler được push vào array:

js
handlers.push(handler);

Array handlers là một holder còn reachable.

Ta có chain:

text
handlers
  ↓
handler
  ↓
outer environment
  ↓
user binding
  ↓
user object

Step 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ừ:

text
Who still holds a reference?

không bắt đầu từ:

text
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

js
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

js
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

js
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ì reader vẫn giữ closure.
  • 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 first vẫn giữ reference tới closure pair.first cũ.
    • Closure đó vẫn cần data.
    • Vì vậy data vẫn có thể reachable qua: text first → closure → environment → data

Transfer Check — Shared Object:

js
const shared = {
  value: 1,
};

function createReader() {
  return () => shared.value;
}

const a = createReader();
const b = createReader();

Hãy trả lời:

  1. a và b có thể cùng giữ khả năng truy cập object nào?
  2. Bỏ a có đủ để kết luận shared không còn reachable không?
  3. Closure ở đây giữ local object hay global binding?
Gợi ý trả lời
  1. Cả hai đều resolve global binding shared, binding này trỏ tới cùng object.
  2. Không. b và global binding shared vẫn còn.
  3. 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:

js
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ẽ:

text
holder
→ closure
→ environment
→ binding
→ object
[Đáp án tham khảo]
text
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 khi getName còn reachable.

Lab 2 — Partial Scaffold: Registry + Unregister ​

Hoàn thiện một registry nhỏ:

js
const handlers = [];

function register(handler) {
  handlers.push(handler);

  return function unregister() {
    // TODO: remove đúng handler khỏi array
  };
}

Dùng:

js
function createHandler(data) {
  return () => data.value;
}

const handler = createHandler({
  value: 42,
});

const unregister = register(handler);

unregister();

Mục tiêu:

  • Hiểu handlers là 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]
js
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 variable handler ở 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:

js
const subscriptions = [];

function subscribe(user) {
  const callback = () => {
    return user.name;
  };

  subscriptions.push(callback);

  // TODO: return cleanup function
}

Implement cleanup:

js
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]
js
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: subscriptions không còn giữ callback sau cleanup. Nếu callback không còn được giữ ở nơi khác, reference chain từ registry đến user có 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 ​

js
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 ​

js
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 đó ​

js
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):

js
const handlers = [];

function openFeature(user) {
  function onAction() {
    return user.name;
  }

  handlers.push(onAction);
}

openFeature({
  name: "Alice",
});

console.log(handlers.length); // 1

Developer chỉ "đóng UI" nhưng không remove callback khỏi handlers.

Evidence (Bằng chứng):

  • Global handlers vẫn reachable.
  • handlers[0] vẫn là callback.
  • Callback dùng parameter binding user.
  • user trỏ 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:

js
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à:

text
long-lived registry
+
missing lifecycle cleanup
+
callback captures user

Fix (Sửa):

Thiết kế registration trả cleanup function:

js
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:

js
subscribe(callback);

Callback có thể capture object lớn.

Hai options:

Option A

js
subscribe(callback);
// không có cách unsubscribe

Option B

js
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:

text
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:

js
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:

text
listeners
→ callback
→ panelData
→ user + cache

Reference 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)

  1. Tự trả lời trước: Closure có tự động gây memory leak không?
  2. Hỏi AI: "Explain how a JavaScript closure can retain object references without claiming every closure is a memory leak."
  3. Inspect: AI có nói "outer function returns, variables stay forever" không?
  4. Challenge: Đưa case có callback registry và yêu cầu AI vẽ reference chain.
  5. 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:

text
reachable holder
→ closure
→ environment
→ captured binding
→ object

và phải tránh:

text
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 retentionTeach Back — holder → closure → environment → object
Vẽ reference chainWorked Example + Lab 1
Phân biệt expected vs unwanted retentionEdge Cases + Production Scenario
Dự đoán reachabilityPrediction — 3/3 đúng
Implement cleanup lifecycleLab 2–3
Debug registry retentionDebug Lab — xác định đúng long-lived holder
Chuẩn bị cho Stage 11Spiral 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
📴 Offline Mode — Content served from cache