Skip to content

Lesson 1.4.6 — Closure + Callback ​

Bài 1.4.6 — Closure + Callback
Callbacks, Captured State & Deferred Execution • 28 phút
0:00 / 0:00

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

FieldValue
Stage1
Module1.4 — Closures
Lesson1.4.6 — Closure + Callback
CompetencyC02 — JavaScript Runtime (C02.5 Closure)
Depth TargetL4 (Debug)
PrerequisitesClosure Formation (1.4.1), Closure Lifetime (1.4.2), Factory Functions (1.4.4), Closure trong Loop (1.4.5), Variable Resolution (1.2.6)
Estimated Cognitive LoadHigh

Out of Scope (Ngoài phạm vi)

  • Event Loop, task queue, microtask queue và timer scheduling (Stage 3)
  • Promise / async-await (Stage 3)
  • Browser event propagation và DOM event model (Stage 4)
  • React callbacks và stale closure (Stage 8)
  • Memory profiling và Garbage Collection internals (Stage 11)

1. Why This Exists (Vì sao cần học) ​

Callback là một function được truyền đi để code khác gọi lại:

js
function run(callback) {
  callback();
}

const name = "Alice";

run(() => {
  console.log(name);
});

Callback được gọi bên trong run, nhưng variable name không được khai báo trong run.

Vậy tại sao callback vẫn đọc được name?

Một hiểu lầm phổ biến là:

"Callback chạy ở đâu thì dùng scope ở đó."

Nếu mental model này đúng, callback trên phải tìm name trong scope của run.

Nhưng JavaScript dùng lexical scope:

Function resolve outer variables dựa trên nơi function được tạo, không phải nơi function được gọi.

Closure chính là cơ chế khiến callback mang theo khả năng truy cập lexical environment của nơi nó được tạo.

Nếu không hiểu điều này, bạn sẽ khó:

  • Trace callback được truyền qua nhiều functions.
  • Hiểu vì sao callback vẫn đọc được state của creator function.
  • Debug callback dùng nhầm shared state.
  • Phân biệt argument của caller với captured variable.
  • Hiểu event handler, async callback và React callback ở các stage sau.

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 Closure Lifetime giữ outer environment reachable khi function còn được tham chiếu.
  • [ ] Phân biệt nơi function được tạo với nơi function được gọi.
  • [ ] Trace được Variable Resolution qua lexical environment chain.
  • [ ] Biết function là value có thể truyền làm argument.
  • [ ] Hiểu nhiều closures có thể dùng cùng một binding hoặc bindings khác nhau.

Nếu thiếu, quay lại Lesson 1.4.1–1.4.5 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 callback là function value được truyền cho code khác gọi.
  2. Trace lexical environment mà callback giữ lại.
  3. Phân biệt lexical scope của callback với scope của function gọi callback.
  4. Dự đoán output khi callback dùng outer variables.
  5. Debug callback đọc sai state vì capture sai binding.
  6. Implement higher-order functions đơn giản nhận callback có closure.
  7. Transfer mental model sang event handler và subscription-style API.

4. Mental Model (Mô hình tư duy) ​

Mental Model — Callback giữ lexical environment nơi nó được tạo

Callback có thể được truyền sang function khác, lưu vào variable khác hoặc gọi ở vị trí khác.

Nhưng outer variable resolution của callback vẫn dựa trên lexical environment nơi callback được tạo.

text
Function Created
      ↓
Lexical Environment Reference
      ↓
Function Passed as Callback
      ↓
Called Somewhere Else
      ↓
Variable Resolution still follows
original lexical environment chain

Ví dụ:

js
const message = "hello";

function execute(callback) {
  const message = "inside execute";
  callback();
}

execute(() => {
  console.log(message);
});

Mental model:

text
Global Environment
├── message = "hello"
└── callback function
    └── outer reference → Global Environment

execute Environment
└── message = "inside execute"

Khi callback chạy bên trong execute, identifier message của callback không tự động đổi sang binding trong execute.

Callback được tạo ở Global Environment, nên nó resolve message theo lexical chain từ đó.

Output:

text
hello

Common Misconception

Không nên nói:

"Callback nhận scope của function gọi nó."

Function call quyết định execution context hiện tại, nhưng lexical scope của function được xác định bởi nơi function được định nghĩa/tạo, không bởi call site thông thường.

5. Core Concepts (Các khái niệm cốt lõi) ​

Essential (Bắt buộc) ​

  • Callback: Function được truyền cho code khác để gọi.
  • Higher-order Function: Function nhận function khác làm argument hoặc return function.
  • Creation Site: Nơi callback được tạo; quyết định lexical environment chain của callback.
  • Call Site: Nơi callback được gọi; không thay đổi lexical scope của callback.
  • Captured Binding: Outer binding callback cần resolve.
  • Callback State: State mà callback truy cập qua closure.

Supporting (Hỗ trợ) ​

  • Callback có thể được tạo inline hoặc tạo trước rồi truyền vào.
  • Parameters của creator function có thể được callback capture.
  • Callback có thể được lưu lại để gọi sau.
  • Cùng một higher-order function có thể nhận callbacks với lexical environments khác nhau.

Awareness (Biết tồn tại) ​

  • Event listener, timer, Promise handler và subscription callback đều dùng mental model này.
  • Async scheduling sẽ được học ở Lesson 1.4.7 và Stage 3.
  • this không theo cùng rule với lexical variable resolution và sẽ học ở Stage 2.

6. Worked Example (Ví dụ phân tích từng bước) ​

Worked Example — Callback được gọi ở nơi khác nhưng giữ scope cũ:

js
function createHandler(userId) {
  const prefix = "USER";

  return function handleClick() {
    return `${prefix}:${userId}`;
  };
}

function runHandler(callback) {
  const prefix = "RUNNER";
  const userId = 999;

  return callback();
}

const handler = createHandler(42);

console.log(runHandler(handler));

Step 1 — createHandler(42) được gọi:

Environment của invocation chứa:

text
userId → 42
prefix → "USER"

Function handleClick được tạo trong environment này.

Step 2 — handleClick được return:

handler giữ reference đến function handleClick.

Function này cần:

text
prefix
userId

nên giữ khả năng resolve hai bindings trong outer environment của createHandler.

Step 3 — runHandler(handler) được gọi:

runHandler tạo environment riêng:

text
callback → handler
prefix → "RUNNER"
userId → 999

Step 4 — callback() được gọi bên trong runHandler:

Call site nằm trong runHandler.

Nhưng function object được gọi chính là handleClick đã được tạo trong createHandler.

Identifier resolution của handleClick đi theo lexical chain của nó:

text
handleClick Environment
      ↓
createHandler Environment
      ↓
prefix = "USER"
userId = 42

Nó không đi sang local bindings prefix và userId của runHandler.

Step 5 — Kết luận:

Output:

text
USER:42

Rule:

Call site không rewrite lexical environment của callback.

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 creation site + lexical environment.

Prediction 1

js
const value = 1;

function run(callback) {
  const value = 2;
  return callback();
}

console.log(run(() => value));

Prediction 2

js
function createCallback(value) {
  return () => value;
}

function run(callback) {
  const value = 100;
  return callback();
}

const callback = createCallback(10);

console.log(run(callback));

Prediction 3

js
let count = 0;

function run(callback) {
  count = count + 1;
  return callback();
}

const callback = () => count;

console.log(run(callback));
console.log(run(callback));
[Đáp án & Giải thích]
  • Prediction 1:

    • Output: 1.
    • Callback được tạo ở global scope, nên identifier value resolve global binding value = 1. Local value = 2 của run không thuộc lexical chain của callback.
  • Prediction 2:

    • Output: 10.
    • Callback được tạo trong invocation createCallback(10) và capture parameter binding value = 10. Local value = 100 trong run không thay thế binding đó.
  • Prediction 3:

    • Output: 1 rồi 2.
    • Callback capture global binding count. run mutate chính global binding trước khi gọi callback, nên callback đọc value hiện tại của cùng binding.

Transfer Check — callback được lưu rồi gọi:

js
const queue = [];

function register(label) {
  queue.push(() => label);
}

register("A");
register("B");

console.log(queue[0]());
console.log(queue[1]());

Hãy trả lời:

  1. Có bao nhiêu invocation của register?
  2. Có bao nhiêu parameter binding label?
  3. Callback đầu tiên giữ binding nào?
  4. Output là gì?
Gợi ý trả lời
  1. Có hai invocation.
  2. Có hai binding label khác nhau.
  3. Callback đầu giữ label của invocation đầu, có value "A".
  4. Output là "A" rồi "B".

8. Implementation Lab (Bài lab thực hành) ​

Lab 1 — Guided: Callback capture config ​

Implement:

js
function createRunner(prefix) {
  // TODO
}

function execute(callback, value) {
  return callback(value);
}

const runner = createRunner("API");

console.log(execute(runner, "start"));
// "[API] start"

Yêu cầu:

  • prefix phải được capture bằng closure.
  • execute không biết prefix.
  • Callback nhận value qua parameter khi được gọi.
[Đáp án tham khảo]
js
function createRunner(prefix) {
  return function (value) {
    return `[${prefix}] ${value}`;
  };
}

function execute(callback, value) {
  return callback(value);
}
  • Giải thích: prefix đến từ lexical environment của createRunner; value là parameter của callback invocation. Hai nguồn dữ liệu khác nhau nhưng được dùng trong cùng function.

Lab 2 — Partial Scaffold: mapValues ​

Hoàn thiện:

js
function mapValues(values, transform) {
  const result = [];

  for (const value of values) {
    // TODO
  }

  return result;
}

const multiplier = 3;

const output = mapValues([1, 2, 3], (value) => {
  return value * multiplier;
});

console.log(output); // [3, 6, 9]

Sau đó trả lời:

  1. value thuộc environment nào?
  2. multiplier thuộc environment nào?
  3. transform được gọi ở đâu?
  4. Call site của transform có làm callback capture local variables của mapValues không?
[Đáp án tham khảo]
js
function mapValues(values, transform) {
  const result = [];

  for (const value of values) {
    result.push(transform(value));
  }

  return result;
}
  • value của callback là parameter của mỗi callback invocation.
  • multiplier được resolve từ environment nơi callback được tạo.
  • Callback được gọi bên trong mapValues.
  • Việc call trong mapValues không làm lexical scope của callback đổi sang scope của mapValues.

Lab 3 — Independent: createValidator ​

Implement:

js
function createValidator(minLength) {
  // TODO
}

function validate(value, validator) {
  return validator(value);
}

const usernameValidator = createValidator(3);

console.log(validate("ab", usernameValidator)); // false
console.log(validate("abcd", usernameValidator)); // true

Constraints:

  • minLength phải private trong creator invocation.
  • Callback nhận string cần validate qua parameter.
  • validate không được biết minLength.
  • Không dùng global mutable state.
[Đáp án tham khảo]
js
function createValidator(minLength) {
  return function (value) {
    return value.length >= minLength;
  };
}

function validate(value, validator) {
  return validator(value);
}
  • Giải thích: validator kết hợp dữ liệu tại creation time (minLength) với dữ liệu tại call time (value).

9. Edge Cases (Các trường hợp ngoại lệ) ​

Edge Case 1 — Tên variable giống nhau không có nghĩa cùng binding ​

js
const value = "global";

function run(callback) {
  const value = "local";
  return callback();
}

const callback = () => value;

Phân tích

Có hai bindings cùng tên value.

Callback được tạo ở global scope nên resolve global binding. Tên giống nhau không làm hai bindings trở thành một.

Khi debug, luôn hỏi:

"Identifier này resolve binding nào?"

Edge Case 2 — Callback parameter shadow captured variable ​

js
const value = 10;

const callback = (value) => {
  return value * 2;
};

console.log(callback(3));

Phân tích

Parameter value của callback shadow outer value = 10.

Output là 6.

Closure không có nghĩa function luôn dùng outer variable. Variable Resolution luôn ưu tiên current lexical environment trước.

Edge Case 3 — Callback capture mutable object ​

js
const config = {
  mode: "A",
};

const callback = () => config.mode;

config.mode = "B";

console.log(callback());

Phân tích

Output là "B".

Callback resolve binding config, rồi đọc property hiện tại của object mà binding trỏ tới.

Closure không snapshot toàn bộ object state tại creation time.

10. Debug Lab (Bài lab gỡ lỗi) ​

Debug Lab — Callback dùng nhầm shared state

Symptom (Triệu chứng): Hai handlers được kỳ vọng giữ hai user khác nhau, nhưng cả hai trả về user cuối cùng.

Reproduction (Tái hiện lỗi):

js
let currentUser;

function createHandler(user) {
  currentUser = user;

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

const aliceHandler = createHandler({ name: "Alice" });
const bobHandler = createHandler({ name: "Bob" });

console.log(aliceHandler()); // "Bob"
console.log(bobHandler()); // "Bob"

Evidence (Bằng chứng):

  • currentUser được khai báo ngoài createHandler.
  • Mỗi call createHandler reassign cùng binding currentUser.
  • Hai returned callbacks dùng identifier currentUser.
  • Sau call thứ hai, shared binding trỏ tới Bob.

Hypothesis (Giả thuyết):

Hai callbacks không capture parameter user; chúng cùng resolve global/shared binding currentUser.

Verification (Xác minh):

Thay callback tạm thời:

js
function createHandler(user) {
  currentUser = user;

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

Nếu output trở thành:

text
Alice
Bob

thì giả thuyết được xác nhận.

Root Cause (Nguyên nhân gốc rễ):

State cần thuộc từng handler nhưng callback lại dùng một shared outer binding.

text
two handlers
+
one shared currentUser binding
=
last assignment wins

Fix (Sửa):

Capture parameter binding của từng invocation:

js
function createHandler(user) {
  return function () {
    return user.name;
  };
}

Prevention (Phòng ngừa):

  • Khi callback cần per-instance/per-registration data, kiểm tra binding có thuộc đúng creator invocation không.
  • Tránh shared mutable binding nếu ownership thực sự thuộc từng callback.
  • Khi nhiều callbacks trả cùng value ngoài dự kiến, kiểm tra chúng có đang resolve cùng một binding không.
  • Trace creation site trước khi nhìn call site.

11. Design Exercise (Bài tập thiết kế giải pháp) ​

Không áp dụng Design Exercise đầy đủ ở depth L4.

Thay vào đó, làm một callback ownership decision.

Bạn cần tạo callback xử lý request với requestId.

Hai options:

Option A — Shared state

js
let currentRequestId;

function createCallback(requestId) {
  currentRequestId = requestId;

  return () => currentRequestId;
}

Option B — Per-invocation capture

js
function createCallback(requestId) {
  return () => requestId;
}

Hãy chọn dựa trên requirement:

Mỗi callback phải nhớ requestId của chính lần đăng ký.

Decision đúng là Option B vì state ownership thuộc từng callback registration, không phải global shared state.

Engineering Decision

Khi thiết kế callback, luôn xác định:

text
Data belongs to:
global?
creator invocation?
callback invocation?
caller?

Sai ownership thường dẫn tới sai binding capture.

12. Production Scenario (Tình huống thực tế) ​

Production Scenario — Event Handler giữ userId

Một UI module tạo handler:

js
function createHandler(userId) {
  return function handleClick() {
    console.log("Open profile:", userId);
  };
}

Sau đó:

js
const aliceHandler = createHandler(101);
const bobHandler = createHandler(202);

Handlers được đưa sang một hệ thống khác để đăng ký event.

Context: Hệ thống event sẽ gọi callback sau này.

Constraint: Event system không cần biết userId.

Mental Model:

text
createHandler(101)
└── Environment A
    └── userId = 101
        ↑
        └── aliceHandler

createHandler(202)
└── Environment B
    └── userId = 202
        ↑
        └── bobHandler

Khi event system gọi:

js
aliceHandler();

callback vẫn resolve userId = 101 từ creation environment.

Production lesson:

Callback API cho phép tách nơi dữ liệu được biết khỏi nơi behavior được kích hoạt.

Đây là pattern nền tảng cho event-driven code, async code và framework callbacks sau này.

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: Vì sao callback được gọi trong function B vẫn dùng variable từ function A nơi nó được tạo?
  2. Hỏi AI: "Does a JavaScript callback use the scope where it is called or where it was created? Explain using lexical environment and closure."
  3. Inspect: AI có dùng dynamic-scope explanation không?
  4. Challenge: Đưa code:
js
const value = 1;

function run(callback) {
  const value = 2;
  return callback();
}

run(() => value);

và yêu cầu AI trace identifier resolution.

  1. Verify: Tự vẽ environment chain trước khi chấp nhận câu trả lời.

Gợi ý

Câu trả lời đúng phải nói rõ:

text
creation site
→ lexical environment
→ closure
→ callback passed
→ callback called elsewhere
→ same lexical resolution chain

Không chấp nhận câu "callback chạy trong run nên thấy local variables của run."

Đáp án tham khảo
  • JavaScript dùng lexical scope, không dùng dynamic scope cho normal variable lookup.
  • Callback function object giữ lexical environment reference từ nơi nó được tạo.
  • Passing function như argument không rewrite outer reference.
  • Calling callback trong function khác cũng không làm callback nhận lexical locals của caller.
  • Local parameter của callback vẫn thuộc callback invocation và được resolve trước outer bindings.

14. Teach Back (Dạy lại) ​

Giải thích cho một đồng nghiệp junior trong 2 phút:

"Callback được gọi trong function khác. Tại sao nó vẫn đọc variable từ nơi nó được tạo, và không tự động thấy local variables của function gọi nó?"

Yêu cầu:

  • Dùng đúng terminology: callback, creation site, call site, lexical environment, closure, variable resolution.
  • Phân biệt callback parameter với captured variable.
  • Không dùng dynamic-scope explanation.
  • Đưa một ví dụ có hai variables cùng tên nhưng khác bindings.
Mô phỏng
  • Bạn nói:
    • Callback chỉ là một function value được truyền sang code khác.
    • Khi function được tạo, lexical environment chain của nó được xác định theo source code / lexical structure nơi nó được tạo.
    • Nếu callback dùng outer variable, closure giữ khả năng resolve binding đó.
    • Khi callback được truyền vào run(callback) rồi gọi bằng callback(), call site chỉ bắt đầu execution của function; nó không thay lexical outer environment của callback.
    • Vì vậy nếu callback được tạo ở global scope và đọc value, nó sẽ resolve global value, ngay cả khi run có một local value cùng tên.
    • Parameter của callback lại thuộc execution/environment của chính callback invocation, nên parameter có thể shadow captured variable.

💡 Hình dung callback mang theo "bản đồ lexical" từ nơi nó sinh ra. Người khác có thể bấm nút chạy callback, nhưng không thay bản đồ đó.

Gợi ý đánh giá bản thân

  • Bạn có chỉ ra được creation site của callback không?
  • Bạn có phân biệt creation site với call site không?
  • Bạn có trace được một variable qua callback parameter → outer environment không?
  • Bạn có giải thích được vì sao callback được truyền qua nhiều function vẫn giữ lexical scope cũ không?

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giá
Giải thích callback + closureTeach Back — creation site vs call site
Trace lexical environment của callbackWorked Example
Dự đoán variable resolutionPrediction — 3/3 đúng
Implement callback có captured configLab 1–3
Phân biệt parameter vs captured bindingEdge Case 2 + Lab 1
Debug shared callback stateDebug Lab — xác định đúng binding ownership
Transfer sang event-style APIProduction Scenario

16. Exit Criteria (Tiêu chí qua bài) ​

  • [ ] Có thể định nghĩa callback bằng function value, không phụ thuộc async.
  • [ ] Có thể phân biệt creation site và call site.
  • [ ] Có thể giải thích callback dùng lexical scope nơi nó được tạo.
  • [ ] Dự đoán đúng 3/3 prediction scenarios.
  • [ ] Có thể phân biệt callback parameter với captured outer binding.
  • [ ] Implement được callback capture config/parameter từ creator function.
  • [ ] Debug được case nhiều callbacks vô tình dùng shared state.
  • [ ] Có thể giải thích vì sao local variable của caller không tự động nằm trong lexical chain của callback.
  • [ ] Transfer được mental model sang event handler/subscription callback.
  • [ ] Không cần dùng Event Loop để giải thích callback closure semantics.

17. Spiral Connection (Liên kết xoắn ốc) ​

Previous (Trước): Closure Formation → Lifetime → Private State → Factory Functions → Closure trong Loop. Bạn đã biết function capture binding nào và bindings có thể shared hoặc riêng.

Current (Hiện tại): Closure + Callback. Bạn học rằng callback có thể được truyền và gọi ở nơi khác nhưng vẫn giữ lexical environment nơi nó được tạo.

Next (Tiếp theo):

  • Lesson 1.4.7 — Closure + Async (callback chạy sau theo thời gian nhưng vẫn giữ captured bindings)
  • Lesson 1.4.8 — Stale Closure (captured state có thể không phải latest state của system)
  • Stage 3 — Event Loop, Promise, async/await và concurrency
  • Stage 4 — Browser events
  • Stage 8 — React callbacks và stale closures
📴 Offline Mode — Content served from cache