Lesson 1.4.6 — Closure + Callback
0. Metadata (Thông tin bài học)
| Field | Value |
|---|---|
| Stage | 1 |
| Module | 1.4 — Closures |
| Lesson | 1.4.6 — Closure + Callback |
| Competency | C02 — JavaScript Runtime (C02.5 Closure) |
| Depth Target | L4 (Debug) |
| Prerequisites | Closure 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 Load | High |
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:
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ể:
- Giải thích callback là function value được truyền cho code khác gọi.
- Trace lexical environment mà callback giữ lại.
- Phân biệt lexical scope của callback với scope của function gọi callback.
- Dự đoán output khi callback dùng outer variables.
- Debug callback đọc sai state vì capture sai binding.
- Implement higher-order functions đơn giản nhận callback có closure.
- 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.
Function Created
↓
Lexical Environment Reference
↓
Function Passed as Callback
↓
Called Somewhere Else
↓
Variable Resolution still follows
original lexical environment chainVí dụ:
const message = "hello";
function execute(callback) {
const message = "inside execute";
callback();
}
execute(() => {
console.log(message);
});Mental model:
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:
helloCommon 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.
thiskhô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ũ:
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:
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:
prefix
userIdnê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:
callback → handler
prefix → "RUNNER"
userId → 999Step 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ó:
handleClick Environment
↓
createHandler Environment
↓
prefix = "USER"
userId = 42Nó không đi sang local bindings prefix và userId của runHandler.
Step 5 — Kết luận:
Output:
USER:42Rule:
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
const value = 1;
function run(callback) {
const value = 2;
return callback();
}
console.log(run(() => value));Prediction 2
function createCallback(value) {
return () => value;
}
function run(callback) {
const value = 100;
return callback();
}
const callback = createCallback(10);
console.log(run(callback));Prediction 3
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
valueresolve global bindingvalue = 1. Localvalue = 2củarunkhông thuộc lexical chain của callback.
- Output:
Prediction 2:
- Output:
10. - Callback được tạo trong invocation
createCallback(10)và capture parameter bindingvalue = 10. Localvalue = 100trongrunkhông thay thế binding đó.
- Output:
Prediction 3:
- Output:
1rồi2. - Callback capture global binding
count.runmutate chính global binding trước khi gọi callback, nên callback đọc value hiện tại của cùng binding.
- Output:
Transfer Check — callback được lưu rồi gọi:
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:
- Có bao nhiêu invocation của
register? - Có bao nhiêu parameter binding
label? - Callback đầu tiên giữ binding nào?
- Output là gì?
Gợi ý trả lời
- Có hai invocation.
- Có hai binding
labelkhác nhau. - Callback đầu giữ
labelcủa invocation đầu, có value"A". - Output là
"A"rồi"B".
8. Implementation Lab (Bài lab thực hành)
Lab 1 — Guided: Callback capture config
Implement:
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:
prefixphải được capture bằng closure.executekhông biếtprefix.- Callback nhận
valuequa parameter khi được gọi.
[Đáp án tham khảo]
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ủacreateRunner;valuelà 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:
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:
valuethuộc environment nào?multiplierthuộc environment nào?transformđược gọi ở đâu?- Call site của
transformcó làm callback capture local variables củamapValueskhông?
[Đáp án tham khảo]
function mapValues(values, transform) {
const result = [];
for (const value of values) {
result.push(transform(value));
}
return result;
}valuecủ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
mapValueskhông làm lexical scope của callback đổi sang scope củamapValues.
Lab 3 — Independent: createValidator
Implement:
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)); // trueConstraints:
minLengthphải private trong creator invocation.- Callback nhận string cần validate qua parameter.
validatekhông được biếtminLength.- Không dùng global mutable state.
[Đáp án tham khảo]
function createValidator(minLength) {
return function (value) {
return value.length >= minLength;
};
}
function validate(value, validator) {
return validator(value);
}- Giải thích:
validatorkế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
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
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
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):
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àicreateHandler.- Mỗi call
createHandlerreassign cùng bindingcurrentUser. - 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:
function createHandler(user) {
currentUser = user;
return function () {
return user.name;
};
}Nếu output trở thành:
Alice
Bobthì 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.
two handlers
+
one shared currentUser binding
=
last assignment winsFix (Sửa):
Capture parameter binding của từng invocation:
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
let currentRequestId;
function createCallback(requestId) {
currentRequestId = requestId;
return () => currentRequestId;
}Option B — Per-invocation capture
function createCallback(requestId) {
return () => requestId;
}Hãy chọn dựa trên requirement:
Mỗi callback phải nhớ
requestIdcủ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:
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:
function createHandler(userId) {
return function handleClick() {
console.log("Open profile:", userId);
};
}Sau đó:
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:
createHandler(101)
└── Environment A
└── userId = 101
↑
└── aliceHandler
createHandler(202)
└── Environment B
└── userId = 202
↑
└── bobHandlerKhi event system gọi:
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)
- 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?
- 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."
- Inspect: AI có dùng dynamic-scope explanation không?
- Challenge: Đưa code:
const value = 1;
function run(callback) {
const value = 2;
return callback();
}
run(() => value);và yêu cầu AI trace identifier resolution.
- 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õ:
creation site
→ lexical environment
→ closure
→ callback passed
→ callback called elsewhere
→ same lexical resolution chainKhô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ằngcallback(), 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 globalvalue, ngay cả khiruncó một localvaluecù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 + closure | Teach Back — creation site vs call site |
| Trace lexical environment của callback | Worked Example |
| Dự đoán variable resolution | Prediction — 3/3 đúng |
| Implement callback có captured config | Lab 1–3 |
| Phân biệt parameter vs captured binding | Edge Case 2 + Lab 1 |
| Debug shared callback state | Debug Lab — xác định đúng binding ownership |
| Transfer sang event-style API | Production 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