Lesson 1.5.5 — Full Execution Trace
0. Metadata (Thông tin bài học)
| Thuộc tính | Giá trị |
|---|---|
| Stage | 1 — JavaScript Execution Model |
| Module | 1.5 — Call Stack & Execution Tracing |
| Lesson | 1.5.5 — Full Execution Trace |
| Competency | C02 — JavaScript Runtime (C02.1–C02.6, trọng tâm C02.6 Call Stack) |
| Depth Target | L4 (Debug / Trace Integration) |
| Prerequisites | Execution Context (Module 1.1), Scope & Lexical Environment (Module 1.2), Hoisting & TDZ (Module 1.3), Closure (Module 1.4), Stack Frames → Nested Calls → Recursion → Stack Unwinding (Module 1.5.1–1.5.4) |
| Cognitive Load | High |
1. Why This Exists (Vì sao cần học)
Từng lesson trước đã cho bạn một mảnh của execution model:
- Execution Context trả lời code đang chạy trong context nào.
- Scope và Lexical Environment trả lời identifier được resolve từ đâu.
- Closure giải thích vì sao function vẫn truy cập được outer environment sau khi outer invocation đã kết thúc.
- Call Stack cho biết invocation nào đang active và control đang ở đâu.
- Nested Calls cho biết caller tạm dừng, callee chạy rồi control quay lại như thế nào.
- Recursion cho thấy nhiều invocation của cùng một function có thể cùng tồn tại.
- Exception cho thấy một invocation có thể rời stack bằng abrupt completion thay vì normal return.
Nhưng production code không xuất hiện dưới dạng từng concept tách rời. Một function có thể vừa đọc outer variable, vừa gọi helper, vừa truyền callback, vừa return value, rồi một callee sâu hơn có thể throw.
Lúc đó câu hỏi thực tế không còn là “đây có phải closure không?” mà là:
Tại đúng thời điểm này, function nào đang chạy, identifier này được lấy từ environment nào, giá trị đang đi theo hướng nào, và nếu lỗi xảy ra thì execution path thay đổi ra sao?
Learning Goal (Mục tiêu học)
Bài này không thêm một cơ chế JavaScript mới. Đây là bài integration: dùng các mental model của toàn Stage 1 để trace một chương trình hoàn chỉnh bằng bằng chứng thay vì đoán theo cảm giác.
2. Prerequisites (Yêu cầu đầu vào)
Trước khi học bài này, bạn cần:
- [ ] Phân biệt được Scope với Execution Context.
- [ ] Vẽ được lexical environment chain cho nested functions.
- [ ] Trace được identifier lookup từ current environment ra outer environment.
- [ ] Giải thích được closure bằng retained outer environment reference.
- [ ] Vẽ được Call Stack của nested calls 3–4 tầng.
- [ ] Trace được return-value flow qua caller → callee → resume.
- [ ] Giải thích được stack growth và stack overflow của recursion.
- [ ] Trace được
throw → propagation → unwind → catch boundary. - [ ] Phân biệt được function definition relationship với function invocation relationship.
Nếu bạn vẫn phải đoán frame nào đang ở đỉnh stack, quay lại Lesson 1.5.1–1.5.2. Nếu bạn còn nhầm outer scope với caller, quay lại Module 1.2 và Module 1.4 trước khi tiếp tục.
3. Learning Objectives (Mục tiêu học tập)
Sau bài này, bạn có thể:
- Trace một chương trình synchronous gồm global code, nested function, local variable, closure, callback và exception.
- Vẽ Call Stack tại một checkpoint bất kỳ và xác định chính xác active invocation.
- Vẽ Environment Graph ở mức conceptual và chỉ ra identifier được resolve từ environment nào.
- Theo dõi Value Flow qua arguments, local bindings, callback arguments và return values.
- Phân biệt static lexical relationship với dynamic call relationship.
- Giải thích vì sao một closure vẫn dùng được outer binding dù outer frame đã rời Call Stack.
- Dự đoán statement nào chạy, statement nào bị skip khi exception xảy ra giữa một execution trace.
- Reconstruct execution path từ output, stack trace và code thay vì đọc code tuyến tính từ trên xuống.
- Debug một trace sai do nhầm Call Stack, Scope Chain hoặc return-value flow.
4. Mental Model (Mô hình tư duy)
Một execution trace đầy đủ không nên cố nhét mọi thứ vào một diagram duy nhất. Hãy dùng ba lens độc lập nhưng liên kết với nhau.
Lens 1 — Call Stack: “Ai đang chạy?”
Global
└── function A()
└── function B()
└── callback()Call Stack là dynamic. Nó thay đổi theo từng function invocation.
Câu hỏi của lens này:
- Invocation nào đang active?
- Frame nào ở trên cùng?
- Caller nào đang tạm dừng?
- Khi
returnhoặcthrow, frame nào rời stack tiếp theo?
Lens 2 — Environment Graph: “Identifier lấy ở đâu?”
Global Environment
├── taxRate
└── createProcessor
↓ lexical relationship
Processor Environment
├── prefix
├── history
└── processOrder
↓ lexical relationship
processOrder Local Environment
├── order
├── onResult
├── calculateSubtotal
└── finalizeEnvironment Graph chủ yếu phản ánh lexical structure của code. Nó không đổi chỉ vì function được gọi từ một caller khác.
Câu hỏi của lens này:
prefixđược resolve từ đâu?taxRateđược resolve từ đâu?- Callback có outer environment nào?
- Binding nào là local, binding nào đến từ outer lexical environment?
Lens 3 — Value Flow: “Giá trị đang đi đâu?”
order.items
↓ argument / read
calculateSubtotal(...)
↓ return 200
subtotal
↓ argument
finalize(200)
↓ compute
220
↓ callback argument + return value
onResult("ORDER:220") / processOrder returns 220Câu hỏi của lens này:
- Argument nào nhận value nào?
- Local binding nào được tạo từ return value?
- Callback nhận value gì?
- Function cuối cùng return gì cho caller?
Mental Model (Mô hình tư duy)
Call Stack ≠ Scope Chain ≠ Value Flow. Ba thứ có liên quan nhưng giải quyết ba câu hỏi khác nhau. Một trace tốt phải biết lúc nào cần nhìn stack, lúc nào cần lookup environment, và lúc nào cần theo value.
Quy trình trace 5 bước
Khi gặp code phức tạp, đi theo thứ tự:
- Locate — xác định statement đang thực thi.
- Stack — xác định active invocation và caller chain.
- Environment — resolve các identifier cần thiết cho statement đó.
- Value — tính value/argument/return đang được truyền.
- Transition — xác định bước tiếp theo là call, return, callback invocation hay exception propagation.
Lặp lại 5 bước này tại từng checkpoint quan trọng.
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
| Khái niệm | Vai trò trong Full Execution Trace |
|---|---|
| Execution point | Statement/expression đang được thực thi tại một thời điểm. |
| Active invocation | Function invocation hiện còn trên Call Stack. |
| Caller / Callee | Quan hệ dynamic giữa invocation gọi và invocation được gọi. |
| Lexical parent | Quan hệ được quyết định bởi nơi function được định nghĩa trong source code. |
| Environment lookup | Quá trình resolve identifier từ current lexical environment ra outer environments. |
| Closure | Function giữ reference tới lexical environment cần thiết sau khi outer invocation kết thúc. |
| Value flow | Đường đi của value qua argument, binding, expression, callback và return. |
| Normal completion | Invocation hoàn tất theo normal control flow và có thể trả value cho caller. |
| Abrupt completion | Execution path bị chuyển bởi throw, khiến exception có thể propagate và unwind stack. |
| Checkpoint | Một điểm được chọn để chụp lại stack + environment + value state phục vụ reasoning. |
Supporting (Hỗ trợ)
- Một function definition có thể được gọi nhiều lần và tạo nhiều invocation khác nhau.
- Caller của một function không quyết định lexical scope của function đó.
- Một callback vẫn là function call bình thường khi nó được invoke synchronously; nó vẫn tạo invocation/frame riêng.
- Closure retention và Call Stack lifetime là hai vấn đề khác nhau.
- Một return value chỉ đi qua caller nếu caller thực sự nhận/return value đó theo code.
- Exception có thể cắt ngang value flow trước khi một assignment hoặc
returnhoàn tất.
Awareness (Biết tồn tại)
- DevTools có thể hiển thị Call Stack, Scope panel và local bindings để hỗ trợ trace.
- Stack trace là evidence của một execution path tại failure point, không phải toàn bộ lịch sử chương trình.
- Source maps, async stack traces và optimized frames có thể làm tooling display phức tạp hơn; các phần này không phải mục tiêu của Stage 1.
- Environment representation thực tế trong engine có thể được optimize; diagram trong lesson là pedagogical mental model.
Out of Scope (Không học trong bài này)
- Event Loop, task, microtask, Promise scheduling,
async/await— Stage 3. this, prototype chain, object model sâu — Stage 2.- V8 stack-frame layout, register allocation, JIT, deoptimization — Stage 11 awareness/depth sau.
- Garbage Collector internals và memory dominator analysis — Stage 11.
- React render stack / hooks lifecycle — Stage 8.
- Production observability architecture, distributed tracing — Stage 13+.
6. Worked Example (Ví dụ phân tích từng bước)
Ta dùng một chương trình chứa đủ các thành phần mà Stage yêu cầu: global, nested function, local variable, closure, callback và exception.
const taxRate = 0.1;
function createProcessor(prefix) {
const history = [];
return function processOrder(order, onResult) {
function calculateSubtotal(items) {
let subtotal = 0;
for (const item of items) {
subtotal += item.price * item.quantity;
}
return subtotal;
}
function finalize(subtotal) {
if (subtotal <= 0) throw new Error("Invalid subtotal");
const total = subtotal + subtotal * taxRate;
const message = `${prefix}:${total}`;
history.push(message);
onResult(message);
return total;
}
try {
const subtotal = calculateSubtotal(order.items);
return finalize(subtotal);
} catch (error) {
onResult(`${prefix}:ERROR`);
return null;
}
};
}
const processOrder = createProcessor("ORDER");
const total = processOrder(
{ items: [{ price: 100, quantity: 2 }] },
(message) => console.log(message)
);
console.log(total);Expected output là ORDER:220 rồi 220.
Phase 1 — Global code tạo createProcessor
Global Environment có taxRate, createProcessor, sau đó sẽ có processOrder và total.
Khi global code gọi createProcessor("ORDER"), Call Stack là:
Global
└── createProcessor("ORDER")Trong invocation này, local bindings quan trọng là prefix = "ORDER" và history = [].
Function processOrder được tạo bên trong createProcessor, nên lexical parent của nó là environment của createProcessor.
createProcessor return function processOrder. Sau return, frame createProcessor rời Call Stack.
Call Stack quay về Global.
Nhưng processOrder vẫn cần prefix và history, nên outer environment cần thiết vẫn reachable qua closure.
Common Misconception (Nhầm lẫn phổ biến)
Không được vẽ createProcessor() vẫn nằm trên Call Stack chỉ vì processOrder còn truy cập được prefix. Frame đã pop; lexical environment cần thiết vẫn được closure giữ reachable.
Checkpoint A — Trước khi gọi processOrder
Call Stack lúc này chỉ có Global.
Environment Graph ở mức conceptual:
Global Environment
├── taxRate = 0.1
├── createProcessor
└── processOrder ──────────────┐
│ closure reference
↓
createProcessor Environment
├── prefix = "ORDER"
└── history = []Value Flow đã hoàn tất một bước: createProcessor("ORDER") → processOrder function value.
Phase 2 — Global gọi closure processOrder(...)
Khi processOrder được gọi, một invocation mới được push:
Global
└── processOrder(order, onResult)Local bindings của invocation này gồm order, onResult, calculateSubtotal, finalize và sau đó là subtotal.
Điểm quan trọng: caller của processOrder là Global, nhưng lexical parent của processOrder vẫn là environment nơi nó được tạo bên trong createProcessor.
Dynamic caller và lexical parent không giống nhau.
Phase 3 — calculateSubtotal(order.items)
processOrder gọi calculateSubtotal, nên stack trở thành:
Global
└── processOrder(...)
└── calculateSubtotal(items)items nhận value từ order.items.
Trong calculateSubtotal, subtotal bắt đầu bằng 0, sau vòng lặp trở thành 200, rồi function return 200.
Value Flow:
order.items
↓
items
↓ loop
subtotal = 200
↓ return
processOrder local `subtotal = 200`Sau return, frame calculateSubtotal pop và control resume trong processOrder.
Phase 4 — finalize(subtotal)
processOrder gọi finalize(200):
Global
└── processOrder(...)
└── finalize(200)Bây giờ ta phải dùng Environment Lens để resolve ba identifier quan trọng: taxRate, prefix và history.
subtotal là parameter local của finalize, nên lookup dừng ngay trong current environment.
taxRate không local trong finalize và cũng không local trong processOrder/createProcessor, nên lookup đi ra Global Environment và lấy 0.1.
prefix và history không local trong finalize, nhưng chúng tồn tại trong outer lexical environment được closure chain giữ từ createProcessor.
Conceptual lookup:
finalize Environment
├── subtotal = 200
└── outer → processOrder Environment
└── outer → createProcessor Environment
├── prefix = "ORDER"
├── history = []
└── outer → Global
└── taxRate = 0.1total được tính thành 220, message thành "ORDER:220", rồi history.push(message) update retained array.
Phase 5 — Callback invocation
finalize chạy onResult(message).
onResult là callback function được truyền từ Global call site. Khi nó được invoke synchronously, đây vẫn là một function call bình thường và tạo invocation riêng.
Call Stack tại callback:
Global
└── processOrder(...)
└── finalize(200)
└── onResult("ORDER:220")
└── console.log("ORDER:220")Sau console.log, callback hoàn tất và pop. Control quay lại finalize tại statement sau onResult(message).
Callback ở Stage 1
Bài này chỉ dùng synchronous callback để luyện execution tracing. Callback async và việc callback chạy sau khi original stack đã trống thuộc Stage 3.
Phase 6 — Return-value flow quay ngược stack
finalize chạy return total;, tức return 220 cho processOrder.
processOrder đang ở câu return finalize(subtotal);, nên nó propagate chính return value 220 ra Global caller.
Value Flow:
finalize
return 220
↓
processOrder
return 220
↓
Global assignment
total = 220Sau đó console.log(total) in 220.
Full success trace — ba lens ghép lại
1. Global calls createProcessor("ORDER")
Stack: Global → createProcessor
Value: prefix = "ORDER", history = []
2. createProcessor returns processOrder closure
Stack: Global
Environment retained: prefix/history
3. Global calls processOrder(...)
Stack: Global → processOrder
4. processOrder calls calculateSubtotal(items)
Stack: Global → processOrder → calculateSubtotal
Value: subtotal 0 → 200 → return 200
5. processOrder calls finalize(200)
Stack: Global → processOrder → finalize
Lookup: subtotal local, prefix/history outer, taxRate global
Value: total = 220, message = "ORDER:220"
6. finalize calls onResult(message)
Stack: Global → processOrder → finalize → callback → console.log
Output: ORDER:220
7. callback returns
Stack: Global → processOrder → finalize
8. finalize returns 220
Stack: Global → processOrder
9. processOrder returns 220
Stack: Global
Value: total = 220
10. console.log(total)
Output: 220Expert Reasoning (Cách reasoning)
Đừng cố “nhớ toàn bộ chương trình”. Tại mỗi checkpoint, chỉ trả lời ba câu: stack hiện tại là gì, identifier cần đọc nằm ở environment nào, value nào đang được tạo/truyền/return.
7. Prediction Exercise (Bài tập dự đoán)
Bài 1 — Failure path
Không chạy code. Với cùng processOrder, gọi:
const result = processOrder(
{ items: [] },
(message) => console.log(message)
);
console.log(result);Hãy trả lời:
calculateSubtotal([])return gì?- Throw site nằm ở đâu?
- Frame nào bị unwound trước khi
catchchạy? - Callback được gọi với value nào?
resultcuối cùng là gì?
Đáp án & Giải thích
calculateSubtotal([]) return 0. processOrder sau đó gọi finalize(0).
Trong finalize, điều kiện subtotal <= 0 đúng nên throw new Error("Invalid subtotal") chạy.
Tại throw site:
Global
└── processOrder(...)
└── finalize(0) ← throwfinalize không có catch, nên frame finalize bị unwound. Exception propagate về processOrder, nơi lời gọi finalize nằm trong try có active catch boundary.
Frame processOrder vẫn active khi catch bắt đầu chạy.
catch gọi callback với "ORDER:ERROR", sau đó return null.
Output là:
ORDER:ERROR
nullBài 2 — Caller không quyết định scope
const label = "GLOBAL";
function createLogger() {
const label = "CLOSURE";
return function log() {
console.log(label);
};
}
function run(callback) {
const label = "RUN";
callback();
}
const log = createLogger();
run(log);Không chạy code. log() in gì và tại sao?
Đáp án & Giải thích
log() in CLOSURE.
Dynamic Call Stack khi callback chạy là:
Global
└── run(log)
└── log()Nhưng lexical parent của log không phải run. log được định nghĩa bên trong createLogger, nên identifier label được resolve theo lexical environment của createLogger, nơi label = "CLOSURE".
Đây là case canonical để chứng minh caller chain không phải scope chain.
Bài 3 — Return-value propagation
function inner() {
return 5;
}
function middle() {
inner();
}
function outer() {
return middle();
}
console.log(outer());Output là gì?
Đáp án & Giải thích
Output là undefined.
inner() return 5, nhưng middle() chỉ gọi inner(); rồi kết thúc mà không return value đó. Vì vậy middle() normal-complete với undefined. outer() return kết quả của middle(), tức undefined.
Call Stack trace đúng không tự động suy ra Value Flow đúng. Bạn phải theo dõi riêng return ở từng boundary.
8. Implementation Lab (Bài lab thực hành)
Lab 1 — Guided: thêm trace checkpoints
Cho code:
function createFormatter(prefix) {
return function format(value, callback) {
const text = `${prefix}:${value}`;
callback(text);
return text.length;
};
}
const format = createFormatter("ID");
const length = format(42, (text) => console.log(text));Hãy viết ra ba checkpoint:
- ngay khi
format()bắt đầu; - ngay khi callback bắt đầu;
- ngay sau khi
format()return.
Mỗi checkpoint phải có Stack / Environment lookup / Value.
Đáp án tham khảo
Checkpoint 1 — format() bắt đầu
Stack:
Global → format(42, console.log)
Environment lookup:
prefix → retained createFormatter environment → "ID"
value → current format environment → 42
Value:
text chưa được tạoCheckpoint 2 — callback bắt đầu
Stack:
Global → format(...) → console.log("ID:42")
Environment lookup:
callback → format parameter
Value:
text = "ID:42"
callback argument = "ID:42"Checkpoint 3 — sau return
Stack:
Global
Value:
format() returned 5
length = 5Outer frame createFormatter không nằm trên Call Stack, nhưng environment chứa prefix vẫn được closure sử dụng.
Lab 2 — Partial Scaffold: điền trace còn thiếu
const fee = 5;
function createCheckout(currency) {
return function checkout(price) {
function addFee(value) {
return value + fee;
}
const total = addFee(price);
return `${currency}:${total}`;
};
}
const checkout = createCheckout("USD");
console.log(checkout(100));Điền các chỗ trống:
A. Sau createCheckout("USD") return:
Call Stack = __________
currency còn reachable vì __________
B. Khi addFee(100) đang chạy:
Call Stack = __________
value = __________
fee resolve từ __________
C. Sau checkout(100) return:
return value = __________Đáp án tham khảo
A. Call Stack chỉ còn Global. currency còn reachable vì returned function checkout giữ reference tới lexical environment cần thiết của createCheckout.
B. Call Stack là Global → checkout(100) → addFee(100). value = 100. fee được resolve từ Global Environment.
C. Return value là "USD:105".
Lab 3 — Independent: tạo execution trace
Viết một chương trình tối đa 30 dòng có đủ:
- một global binding;
- một factory function;
- một returned closure;
- một nested helper;
- một synchronous callback;
- một
throwcondition; - một
catchboundary; - một final return value.
Sau đó tự tạo ít nhất 5 checkpoints. Mỗi checkpoint phải ghi:
Execution point
Call Stack
Relevant Environment Lookup
Relevant Value Flow
Next TransitionConstraint: không dùng Promise, timer, DOM API hoặc async/await.
9. Edge Cases (Các trường hợp ngoại lệ)
Edge Case 1 — Scope Chain không đi theo Call Stack
const x = "global";
function createFn() {
const x = "closure";
return function read() {
return x;
};
}
const read = createFn();function caller() {
const x = "caller";
return read();
}
console.log(caller());caller() nằm ngay dưới read() trên Call Stack, nhưng read() vẫn return "closure", không phải "caller".
Common Misconception (Nhầm lẫn phổ biến)
“Function tìm biến ở caller trước” là mental model sai với lexical scope. JavaScript resolve identifier theo lexical environment chain, không đi ngược Call Stack để tìm local variable của caller.
Edge Case 2 — Closure environment tồn tại không có nghĩa outer frame tồn tại
Sau khi factory return, outer frame rời stack. Nếu returned closure vẫn cần outer binding, environment cần thiết có thể tiếp tục reachable.
Không được vẽ:
Global
└── factory() ← sai nếu factory đã return
└── closure()khi thực tế closure được gọi sau đó từ Global. Stack đúng tại invocation mới có thể là:
Global
└── closure()Environment Graph mới là nơi biểu diễn retained lexical relationship.
Edge Case 3 — Callback có thể là callee nhưng không phải lexical child
Một callback được invoke bên trong finalize() sẽ nằm trên finalize() trong Call Stack tại thời điểm chạy. Nhưng lexical parent của callback phụ thuộc nơi callback được định nghĩa, không phải nơi callback được gọi.
Edge Case 4 — Exception cắt value flow
Nếu expression const result = risky(); gọi risky() và risky() throw trước khi return, binding result không nhận được normal return value từ call đó. Trace không được ghi “result = undefined” chỉ vì callee không return; exception propagation là một completion khác normal undefined return.
Edge Case 5 — Tooling snapshot không phải formal model
DevTools có thể hiển thị optimized frames, hidden/internal frames hoặc scope groups theo cách riêng của engine/tooling. Dùng tooling như evidence, nhưng reasoning phải dựa trên semantics đã học: invocation, lexical lookup, value flow và completion path.
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng)
Một formatter đôi lúc in đúng message nhưng caller lại nhận undefined. Developer nghi ngờ closure hoặc callback “ăn mất return value”.
Reproduction (Tái hiện lỗi)
function createFormatter(prefix) {
return function format(value, onFormatted) {
function buildMessage() {
return `${prefix}:${value}`;
}
const message = buildMessage();
onFormatted(message);
message.length;
};
}
const format = createFormatter("ID");
const result = format(42, console.log);
console.log("result:", result);Observable output:
ID:42
result: undefinedEvidence (Bằng chứng)
- Closure lookup hoạt động vì output chứa đúng
prefix = "ID". buildMessage()return đúng"ID:42".- Callback nhận đúng message.
format()chạy đến cuối function mà khôngthrow.- Statement cuối là
message.length;, không phảireturn message.length;.
Hypothesis (Giả thuyết)
Bug nằm ở Value Flow, không phải Call Stack hay Scope Chain. Value 5 được evaluate nhưng không được return qua boundary của format().
Verification (Xác minh)
Trace value:
buildMessage()
↓ return "ID:42"
message = "ID:42"
↓
message.length evaluates to 5
↓
NO return statement
↓
format() normal-completes with undefined
↓
result = undefinedRoot Cause (Nguyên nhân gốc rễ)
Developer đã đọc execution trace theo kiểu “giá trị cuối cùng trong function sẽ tự động được trả về”. JavaScript function body không implicit-return expression như arrow expression body.
Fix (Sửa lỗi)
Thêm return message.length;.
Prevention (Phòng ngừa)
- Khi output đúng nhưng caller nhận sai value, trace return boundary riêng biệt.
- Không suy ra return value từ “expression cuối cùng”.
- Tách diagnosis thành ba hypothesis: stack/control, environment lookup, value flow.
- Chỉ sửa phần mental model đã bị evidence bác bỏ.
Debugging Lens (Góc nhìn gỡ lỗi)
Một bug execution-model thường trở nên dễ hơn khi bạn hỏi: Sai ở control flow, sai ở identifier lookup, hay sai ở value propagation? Đừng debug cả ba cùng lúc nếu evidence đã loại trừ hai phần.
11. Design / Decision Exercise (Bài tập lựa chọn cách trace)
Bạn cần debug một function 25 dòng có nested helper, closure và callback. Có hai cách ghi chú trace:
| Cách | Đặc điểm | Rủi ro |
|---|---|---|
| Một diagram duy nhất | Cố đưa stack, scope và values vào cùng một hình | Nhanh rối, dễ nối nhầm caller với lexical parent |
| Ba lens riêng | Stack / Environment / Value được ghi riêng tại checkpoint | Tốn thêm vài dòng nhưng dễ kiểm chứng từng assumption |
Câu hỏi: Ở Stage 1, nên chọn cách nào làm default?
Đáp án tham khảo
Ưu tiên ba lens riêng.
Ở depth L4, mục tiêu là trace và debug chính xác. Một diagram duy nhất có thể hữu ích sau khi đã rất thành thạo, nhưng với learner đang xây mental model, nó dễ tạo một graph “trông hợp lý” nhưng trộn ba loại relationship khác nhau.
Default workflow nên là:
- Call Stack để xác định active invocation.
- Environment Graph để resolve identifier.
- Value Flow để theo argument/return.
- Chỉ ghép chúng ở phần kết luận khi cần.
Decision Lens (Góc nhìn quyết định)
Diagram tốt không phải diagram chứa nhiều thông tin nhất. Diagram tốt là diagram làm một loại relationship đủ rõ để bạn dự đoán behavior và bác bỏ hypothesis sai.
12. Production Scenario (Tình huống thực tế)
Một checkout utility được tổ chức bằng factory để giữ config:
function createCheckout(currency, onError) {
return function checkout(cart, onSuccess) {
function calculateTotal() {
if (cart.items.length === 0) {
throw new Error("Empty cart");
}
return cart.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
}
try {
const total = calculateTotal();
onSuccess(`${currency}:${total}`);
return total;
} catch (error) {
onError(error.message);
return null;
}
};
}Context: currency và onError được cấu hình một lần, còn checkout được gọi nhiều lần với cart khác nhau.
Constraint: team thấy lỗi “callback thành công không chạy” cho một số cart và nghi callback bị mất closure.
Symptom: onError("Empty cart") chạy, onSuccess(...) không chạy và checkout() return null.
Hãy dùng ba lens để chẩn đoán.
Đáp án tham khảo
Call Stack / Control: với cart rỗng, checkout() gọi calculateTotal(). Helper throw trước khi return. calculateTotal bị unwound và exception quay về catch boundary trong checkout. Vì control đã chuyển sang catch, onSuccess(...) không chạy.
Environment: currency và onError vẫn resolve bình thường từ retained environment của createCheckout. Không có evidence cho thấy closure bị mất.
Value Flow: total không nhận normal return value vì calculateTotal() throw. catch gọi onError(error.message) và checkout() return null.
Root cause nằm ở expected failure path, không phải closure.
Production Risk (Rủi ro production)
Nếu team chỉ nhìn “callback không chạy” rồi sửa closure/callback wiring, họ có thể phá code đang đúng. Execution tracing giúp phân biệt symptom ở một layer với root cause ở control flow layer khác.
13. AI-Assisted Exercise (Bài tập với AI)
Level 2 — Audit AI Trace
Cho AI đoạn code sau:
const x = "global";
function factory() {
const x = "closure";
return function read(callback) {
callback(x);
return x.length;
};
}
function run(fn) {
const x = "caller";
return fn(console.log);
}
const read = factory();
console.log(run(read));Thực hiện theo thứ tự:
- Tự vẽ Call Stack tại thời điểm
console.logcallback chạy. - Tự vẽ lexical lookup của
xbên trongread. - Tự tính return-value flow tới
console.log(run(read)). - Hỏi AI tạo full execution trace.
- Audit xem AI có mắc một trong ba lỗi sau không:
- cho
read()lấyx = "caller"từrun(); - giữ frame
factory()trên stack khiread()chạy; - quên
read()return7vàrun()propagate7.
- cho
- Chỉ chấp nhận trace nếu cả Stack / Environment / Value đều nhất quán.
Đáp án tham khảo
Khi callback chạy, Call Stack conceptual là:
Global
└── run(read)
└── read(console.log)
└── console.log("closure")x trong read resolve từ retained environment của factory, nên value là "closure".
read() return x.length, tức 7. run() dùng return fn(console.log);, nên propagate 7. Final output là closure rồi 7.
factory() không còn trên stack tại thời điểm này.
14. Teach Back (Dạy lại)
Trong 3 phút, giải thích cho một junior câu sau:
“Call Stack cho biết ai gọi ai lúc runtime; Scope Chain cho biết identifier được tìm ở đâu; hai graph có thể nhìn hoàn toàn khác nhau.”
Dùng đoạn code này làm ví dụ:
const value = "global";
function makeReader() {
const value = "closure";
return () => value;
}
function execute(fn) {
const value = "caller";
return fn();
}
const read = makeReader();
console.log(execute(read));Bắt buộc giải thích đủ:
- Call Stack khi
read()chạy. - Lexical parent của
read. - Vì sao
value = "caller"không được dùng. - Vì sao frame
makeReader()không cần còn trên stack. - Value nào đi từ
read()→execute()→console.log().
Mô phỏng
Khi read() chạy, Call Stack là Global → execute(read) → read(). Đây là dynamic caller chain: execute đang gọi read.
Nhưng read được định nghĩa bên trong makeReader, nên lexical environment của nó nối tới environment của makeReader, nơi value = "closure". JavaScript không đi ngược stack để tìm value trong local scope của caller execute, nên "caller" không được dùng.
makeReader() đã return từ trước nên frame của nó không còn trên Call Stack. Closure chỉ cần retained lexical environment cần thiết, không cần giữ invocation frame active.
read() return "closure"; execute() dùng return fn(); nên propagate value đó; console.log() nhận "closure".
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Depth (Độ sâu) |
|---|---|---|
| Trace full synchronous execution | Multi-checkpoint Trace | L4 |
| Vẽ Call Stack tại execution point | Prediction / Trace | L3–L4 |
| Resolve identifier bằng Environment Graph | Explain + Trace | L3–L4 |
| Theo dõi argument / callback / return values | Value-flow Trace | L3–L4 |
| Phân biệt lexical parent và dynamic caller | Compare + Explain | L4 |
| Giải thích closure sau khi outer frame pop | Explain | L3–L4 |
| Trace exception trong full flow | Debug Trace | L4 |
| Reconstruct root cause từ evidence | Debug | L4 |
| Audit một execution trace sai | Error Analysis | L4 |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể trace một chương trình synchronous 25–40 dòng bằng ít nhất 5 checkpoints mà không chạy code trước.
- [ ] Tại mỗi checkpoint, có thể ghi đúng Execution Point + Call Stack + Environment Lookup + Value Flow + Next Transition.
- [ ] Có thể phân biệt chính xác Call Stack, Scope/Lexical Environment Chain và Value Flow mà không dùng chúng thay thế cho nhau.
- [ ] Có thể giải thích vì sao caller không quyết định lexical scope của callee.
- [ ] Có thể vẽ đúng case closure được gọi sau khi outer factory frame đã rời stack.
- [ ] Có thể theo dõi return value qua ít nhất 3 function boundaries và phát hiện chỗ value bị mất vì thiếu
return. - [ ] Có thể trace synchronous callback như một invocation bình thường mà không kéo Event Loop vào explanation.
- [ ] Có thể trace exception từ throw site đến catch boundary trong cùng full execution trace.
- [ ] Có thể dùng output/stack trace/code để bác bỏ ít nhất một hypothesis sai về scope, stack hoặc value flow.
- [ ] Có thể giải thích full trace cho người khác bằng causal reasoning thay vì chỉ đọc lại từng dòng code.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Lesson 1.5.1–1.5.4 đã tách Call Stack thành từng năng lực: frame lifecycle, nested caller/callee flow, recursion/stack growth và exception/stack unwinding. Các Module 1.1–1.4 đã xây Execution Context, Scope, Lexical Environment, Hoisting/TDZ và Closure.
Current (Hiện tại): Full Execution Trace ghép các mental model đó thành một workflow duy nhất: Execution Point → Call Stack → Environment Lookup → Value Flow → Transition. Đây là checkpoint quan trọng của Stage 1: bạn không chỉ biết từng khái niệm mà có thể dùng chúng cùng nhau để dự đoán và debug behavior.
Next (Tiếp theo): Sau Module 1.5, Stage 1 chuyển sang Integration Lab / Project 1 — Closure-based State Library để chứng minh execution + scope + closure bằng implementation thực tế. Execution tracing sau đó được spiral mạnh ở Stage 3 khi synchronous Call Stack gặp Event Loop/Promise, ở Stage 8 với React render/hooks, và ở Stage 11–14 với memory, profiling, incident debugging và engineering judgment.