Skip to content

Lesson 1.1.5 — Execution Context Stack ​

Bài 1.1.5 — Execution Context Stack
Context Lifecycle, Push/Pop & Nested Function Calls • 23 phút
0:00 / 0:00

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

TrườngGiá trị
Stage1 — JavaScript Execution Model
Module1.1 — Execution Context
Lesson1.1.5 — Execution Context Stack
CompetencyC02 — JavaScript Runtime
DepthL2–L3 (Explain → Use)
PrerequisitesLesson 1.1.1–1.1.4 — Execution Context, Global FEC, Function FEC, Creation/Execution Phase
Thời gian ước tính30–40 phút

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

Bạn đã biết mỗi lần gọi hàm tạo ra một Function Execution Context mới. Bạn cũng biết context được hủy khi hàm return.

Nhưng câu hỏi này sẽ khiến bạn dừng lại:

js
function a() {
  b();
  console.log("a done");
}

function b() {
  c();
  console.log("b done");
}

function c() {
  console.log("c done");
}

a();

Nếu a() gọi b(), và b() gọi c(), thì khi c() đang chạy:

  • a() đã kết thúc chưa?
  • b() còn tồn tại không?
  • Khi c() return, làm sao JavaScript biết phải quay lại b() chứ không phải a() hay Global?

Nếu không có mental model về stack, bạn sẽ:

  • Nghĩ rằng các hàm chạy "song song" hoặc hàm ngoài biến mất khi hàm trong chạy
  • Không hiểu tại sao DevTools hiển thị một "cột" tên hàm khi có lỗi
  • Không thể trace code có 3–4 lớp lồng nhau một cách có hệ thống
  • Không thể học Module 1.5 (Call Stack & Exception) và Stage 3 (Async)

2. Prerequisites (Yêu cầu đầu vào) ​

Bạn cần đã học:

  • Lesson 1.1.1 — Execution Context là gì?
  • Lesson 1.1.2 — Global Execution Context
  • Lesson 1.1.3 — Function Execution Context
  • Lesson 1.1.4 — Creation Phase vs Execution Phase

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 Execution Context Stack hoạt động theo cơ chế LIFO (Last In, First Out)
  2. Trace thứ tự push và pop các context khi có nested function calls
  3. Dự đoán số lượng context đồng thời tồn tại trên stack tại một thời điểm
  4. Giải thích tại sao khi một hàm return, engine biết phải quay lại hàm nào

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

Execution Context Stack = Chồng container theo quy tắc LIFO

Khi một hàm gọi hàm khác, JavaScript không xóa context cũ. Nó chỉ tạm dừng context cũ, đặt context mới lên trên, và chạy context mới. Khi context mới kết thúc, nó bị lấy ra, và context cũ tiếp tục từ chỗ dừng lại.

Quy tắc:

  • Push: Gọi hàm → đẩy context mới lên đỉnh stack
  • Pop: Return → lấy context trên cùng ra khỏi stack
  • Chỉ context trên cùng được chạy tại bất kỳ thời điểm nào
  • Global Execution Context luôn nằm ở đáy stack

Đây là mental model pedagogical. Chi tiết về stack frames, pointer, và register sẽ không được dạy ở depth này.

text
Gọi a()          Gọi b() trong a    Gọi c() trong b
    ↓                  ↓                  ↓
┌─────────┐        ┌─────────┐        ┌─────────┐
│ Global  │        │ Global  │        │ Global  │
├─────────┤   →    ├─────────┤   →    ├─────────┤
│         │        │   a()   │        │   a()   │
│         │        │ (paused)│        │ (paused)│
│         │        ├─────────┤        ├─────────┤
│         │        │         │        │   b()   │
│         │        │         │        │ (paused)│
│         │        │         │        ├─────────┤
│         │        │         │        │   c()   │
│         │        │         │        │ (active)│
└─────────┘        └─────────┘        └─────────┘

Quy tắc vàng

Hàm ngoài không kết thúc khi hàm trong bắt đầu. Nó chỉ bị tạm dừng (paused).

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

Essential (Bắt buộc) ​

  • Execution Context Stack: Cấu trúc LIFO chứa tất cả các context đang tồn tại, với Global ở đáy và active context ở đỉnh
  • Push: Thêm Function Execution Context mới vào đỉnh stack khi hàm được gọi
  • Pop: Xóa Function Execution Context khỏi đỉnh stack khi hàm return hoặc kết thúc
  • Paused context: Các context bên dưới đỉnh stack tạm dừng thực thi và chờ context trên cùng kết thúc

Supporting (Hỗ trợ) ​

  • Active context: Chỉ có context ở đỉnh stack được thực thi tại một thời điểm
  • Stack depth: Số lượng context đồng thời tồn tại trên stack

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

  • Stack trace trong DevTools là "ảnh chụp" stack tại thời điểm lỗi xảy ra
  • Call Stack chi tiết (stack frames, line numbers) thuộc Module 1.5

Out of Scope (Không học trong bài này) ​

  • Recursion và stack overflow (Module 1.5)
  • Exception và stack unwinding (Module 1.5)
  • Event Loop / Task Queue (Stage 3)
  • Bất kỳ engine internals nào sâu hơn mental model L2–L3

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

Xem đoạn code sau:

js
function first() {
  second();
  console.log("first done");
}

function second() {
  third();
  console.log("second done");
}

function third() {
  console.log("third done");
}

first();
console.log("global done");

Bước 1 — Quan sát Có 3 hàm lồng nhau: first gọi second, second gọi third. Mỗi hàm đều có console.log sau lời gọi hàm bên trong.

Bước 2 — Phân loại

  • first() tạo FEC #1
  • second() tạo FEC #2 (bên trong first)
  • third() tạo FEC #3 (bên trong second)

Bước 3 — Lý luận từng bước

text
1. Global EC tạo (đáy stack)

2. first() được gọi
   → PUSH FEC first
   Stack: [Global, first]
   → Chạy body first
   → Gặp second() — first PAUSED

3. second() được gọi
   → PUSH FEC second
   Stack: [Global, first, second]
   → Chạy body second
   → Gặp third() — second PAUSED

4. third() được gọi
   → PUSH FEC third
   Stack: [Global, first, second, third]
   → Chạy body third
   → console.log("third done") → in ra
   → third return
   → POP FEC third
   Stack: [Global, first, second]

5. second RESUME từ chỗ pause
   → console.log("second done") → in ra
   → second return
   → POP FEC second
   Stack: [Global, first]

6. first RESUME từ chỗ pause
   → console.log("first done") → in ra
   → first return
   → POP FEC first
   Stack: [Global]

7. Global RESUME
   → console.log("global done") → in ra

Bước 4 — Kết luận Output là:

text
third done
second done
first done
global done

Các context không biến mất khi hàm con chạy. Chúng bị tạm dừng, chờ hàm con pop khỏi stack, rồi tiếp tục.

7. Prediction Exercise (Bài tập dự đoán) ​

Đừng chạy code. Đọc và trả lời:

js
function outer() {
  console.log("outer start");
  inner();
  console.log("outer end");
}

function inner() {
  console.log("inner start");
  core();
  console.log("inner end");
}

function core() {
  console.log("core");
}

outer();

Câu hỏi:

  1. Khi core() đang chạy, có bao nhiêu Execution Context đồng thời tồn tại trên stack?
  2. outer() đã kết thúc chưa khi core() chạy? Nếu chưa, trạng thái của nó là gì?
  3. Output cuối cùng theo thứ tự là gì?
[Đáp án & Giải thích]
  • Bạn nghĩ

    1. 4 context: Global, outer, inner, core.
    2. outer() chưa kết thúc. Nó đang bị tạm dừng (paused) chờ inner() return.
    3. Output: outer start → inner start → core → inner end → outer end.
  • Giải thích

    • Khi core() chạy, stack là: [Global, outer, inner, core]. Cả 4 đều tồn tại.
    • outer() bị pause tại dòng inner();. Nó chưa chạy đến console.log("outer end").
    • Thứ tự output phản ánh LIFO: push sâu nhất chạy xong trước, rồi pop, rồi resume từng lớp.
    • Sai lầm phổ biến: nghĩ outer() đã "xong" khi inner() bắt đầu. Thực tế outer() chỉ bị tạm dừng (paused).

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

Lab 1 — Trace Push và Pop (Guided) ​

Cho đoạn code sau. Hãy viết ra trạng thái stack (tên các context) sau mỗi lần push hoặc pop:

js
function alpha() {
  beta();
}

function beta() {
  gamma();
}

function gamma() {
  console.log("done");
}

alpha();

Gợi ý

  • Bắt đầu: [Global]
  • Sau alpha() được gọi: push alpha → [Global, alpha]
  • Sau beta() được gọi bên trong alpha: push beta → [Global, alpha, beta]
  • Tiếp tục cho đến khi stack chỉ còn [Global]
[Đáp án tham khảo]
Thao tácStack sau thao tác
Global EC tạo[Global]
Gọi alpha()[Global, alpha]
Gọi beta()[Global, alpha, beta]
Gọi gamma()[Global, alpha, beta, gamma]
gamma return → pop[Global, alpha, beta]
beta return → pop[Global, alpha]
alpha return → pop[Global]

Lab 2 — Tạo Stack với Độ Sâu Chính Xác (Independent) ​

Viết một đoạn code (không dùng recursion) sao cho tại thời điểm sâu nhất, Execution Context Stack có đúng 5 context (1 Global + 4 Function).

[Đáp án tham khảo]
js
function a() {
  b();
}

function b() {
  c();
}

function c() {
  d();
}

function d() {
  console.log("deep");
}

a();
  • Giải thích
    • [Global]
    • Gọi a() → [Global, a]
    • Gọi b() → [Global, a, b]
    • Gọi c() → [Global, a, b, c]
    • Gọi d() → [Global, a, b, c, d] → 5 context

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

Case 1 — Hàm không gọi hàm khác: stack chỉ có 2 lớp ​

js
function simple() {
  console.log("simple");
}

simple();
  • What fails? Learner nghĩ rằng mọi lúc đều có nhiều context lồng nhau
  • Why? Không phải lúc nào stack cũng sâu. Khi simple() chạy, stack chỉ có [Global, simple]
  • How to observe? Đặt debugger; bên trong simple() và mở DevTools → Call Stack chỉ hiển thị 2 frames
  • How to fix/decide? Không cần fix. Nhưng cần hiểu để không over-engineer mental model.

Case 2 — Return về đúng chỗ nhờ stack ​

js
function taskA() {
  taskB();
  console.log("A");
}

function taskB() {
  return;
}

taskA();
  • What fails? Nếu không có stack, engine sẽ không biết taskB return về đâu
  • Why? Stack ghi nhớ rằng taskB được gọi từ dòng nào của taskA. Khi taskB pop, taskA resume từ đúng dòng đó
  • How to observe? Output là "A" — chứng tỏ taskA đã tiếp tục sau khi taskB return
  • How to fix/decide? Không cần fix. Đây là behavior đúng. Nhưng nếu bạn thấy code "nhảy lung tung" khi return, hãy nghĩ đến việc trace stack.

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

Symptom (Triệu chứng): Một developer junior xem DevTools Console khi có lỗi và thấy stack trace như sau:

text
Error: invalid input
    at parseInput (app.js:15)
    at validateForm (app.js:10)
    at handleSubmit (app.js:5)
    at global (app.js:20)

Developer thắc mắc: "Tôi chỉ gọi handleSubmit(), sao lỗi lại hiển thị cả parseInput và validateForm? Tôi không gọi trực tiếp chúng mà."

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

js
function handleSubmit() {
  validateForm();
}

function validateForm() {
  parseInput();
}

function parseInput() {
  throw new Error("invalid input");
}

handleSubmit();

Evidence (Bằng chứng): Stack trace hiển thị 4 tên hàm mặc dù developer chủ đích gọi 1 hàm.

Hypothesis (Giả thuyết): Developer nhầm lẫn stack trace với "call history log". Stack trace không phải là danh sách các hàm đã chạy xong. Nó là ảnh chụp Execution Context Stack tại thời điểm lỗi xảy ra. parseInput đang ở đỉnh stack (active), còn validateForm và handleSubmit đang bị paused bên dưới.

Verification (Xác minh): Thêm console.log sau mỗi lời gọi — thấy các hàm ngoài thực sự chưa chạy xong khi lỗi xảy ra.

Root Cause (Nguyên nhân gốc rễ): Không hiểu Execution Context Stack. Khi handleSubmit() gọi validateForm(), rồi validateForm() gọi parseInput(), cả 3 context đều đồng thời tồn tại. Lỗi ở parseInput "đóng băng" toàn bộ stack để hiển thị.

Prevention (Phòng ngừa):

  • Hiểu stack trace là snapshot của stack, không phải log
  • Khi đọc stack trace, đọc từ trên xuống: lỗi ở đâu → được gọi từ đâu → được gọi từ đâu nữa
  • Độ sâu stack trace giúp trace ngược đến nguồn gốc lỗi
  • Đừng chỉ sửa logic ở hàm đỉnh stack (nơi lỗi "nổ"), hãy kiểm tra dữ liệu và luồng gọi qua từng lớp bên dưới để tìm root cause thực sự

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

Không áp dụng ở Depth L2–L3.

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

Context: Một ứng dụng web có form đăng nhập. Người dùng bấm submit, dữ liệu đi qua 4 lớp: handleSubmit → validateForm → sanitizeInput → parseJSON.

Constraint: Production crash với lỗi SyntaxError: Unexpected token nhưng stack trace dài 4 dòng.

Symptom: PM yêu cầu developer tìm lỗi. Developer chỉ nhìn dòng đầu tiên của stack trace (parseJSON) và sửa logic parse. Nhưng lỗi thực sự là sanitizeInput đã truyền sai định dạng xuống parseJSON.

Decision cần đưa ra: Dùng kiến thức về Execution Context Stack, giải thích tại sao developer phải đọc toàn bộ stack trace từ trên xuống dưới, không chỉ dòng trên cùng.

Gợi ý

Stack trace hiển thị cả 4 context đồng thời tồn tại. Dòng trên cùng (parseJSON) là nơi lỗi "nổ". Các dòng dưới là nơi dữ liệu được truyền xuống. Để tìm root cause, phải trace ngược từ parseJSON → sanitizeInput → validateForm → handleSubmit để xem dữ liệu bị hỏng từ lớp nào.

13. AI-Assisted Exercise (Bài tập với AI) ​

Level A — Ask

  1. Tự trả lời trước: Viết ra giấy: "Khi function A gọi function B, function A có biến mất khỏi stack không? Tại sao JavaScript biết quay lại A sau khi B kết thúc?"
  2. Hỏi AI: Dùng prompt: "In JavaScript, when function A calls function B, does function A's execution context get removed from the stack? How does JavaScript know where to return after B finishes?"
  3. So sánh: AI có nhắc đến LIFO và push/pop không? AI có giải thích rằng A bị paused chứ không bị xóa không? AI có dùng từ mơ hồ như "JavaScript remembers" mà không giải thích mechanism không?
  4. Verify: Mở MDN "Execution context" hoặc tìm "call stack LIFO JavaScript". Kiểm chứng xem AI có bỏ sót điểm nào.

Gợi ý

Để ý xem AI có nói "A waits for B to finish" không. Câu trả lời này đúng ở bề mặt nhưng không giải thích tại sao A biết tiếp tục từ đâu. Nếu AI không nhắc đến stack hoặc nói "the context is saved somewhere" mà không nói rõ "pushed below B on the stack", đó là điểm mù. Nhiều AI cũng sẽ không phân biệt "paused" vs "finished".

Đáp án tham khảo
  • Bạn nghĩ Function A không biến mất. Context của A bị tạm dừng và nằm bên dưới context của B trên stack. Khi B return, B bị pop khỏi đỉnh stack, và A — đang nằm ngay dưới — trở thành context trên cùng và tiếp tục chạy từ chỗ pause. JavaScript không cần "nhớ" đặc biệt vì stack structure đã đảm bảo điều này.

  • AI trả lời

    • "When function A calls function B, A's execution context is not removed. It stays on the call stack."
    • "B's execution context is pushed on top of A's context."
    • "When B finishes executing, it is popped off the stack, and A resumes execution."
    • "JavaScript uses a call stack to keep track of where to return after a function completes."
    • "The stack follows LIFO — Last In, First Out — so the most recently called function is always the one that finishes first."
  • So sánh

    • AI đúng về cơ chế chung: A không bị xóa, B đẩy lên trên, pop rồi resume.
    • AI dùng từ "stays on the call stack" — đúng nhưng hơi mơ hồ. AI không nhấn mạnh rằng A bị paused (tạm dừng) ở dòng cụ thể. "Stays" có thể hiểu là A vẫn đang chạy song song.
    • AI nói "JavaScript uses a call stack to keep track of where to return" — đúng nhưng mechanism-level. Ở L2–L3, chúng ta cần hiểu Execution Context Stack là container stack, không chỉ "call stack" như một black box.
  • Điểm AI nói sai hoặc quá mơ hồ

    • AI thường dùng "call stack" và "execution context" thay thế cho nhau mà không phân biệt. Trong curriculum này, Execution Context Stack là stack các container context; Call Stack (Module 1.5) là khái niệm liên quan nhưng tập trung vào stack frames và debugging. AI gộp hai khái niệm này.
    • AI thường không nhắc đến Global Execution Context luôn ở đáy stack. Điều này quan trọng để hiểu tại sao script không kết thúc khi một hàm đang chạy.
    • AI có thể nói "the function is paused" nhưng không giải thích paused ở dòng nào — điểm then chốt để trace code.
  • Kết luận Nếu bạn phát hiện AI dùng "call stack" như một từ chung chung để giải thích mọi thứ, và không phân biệt rằng context bị paused (chứ không phải "đợi" một cách chủ động), bạn đã hiểu sâu hơn AI về Execution Context Stack. Điều này sẽ cực kỳ quan trọng khi bạn học Module 1.5 (Call Stack & Exception), nơi stack trace được phân tích chi tiết, và Stage 3 (Async), nơi callback không push lên cùng một stack.

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

Yêu cầu: Giả sử một junior developer hỏi: "Tôi gọi a(), bên trong a() gọi b(), bên trong b() gọi c(). Khi c() chạy, a() đã xong chưa? Nếu chưa, sao c() biết phải quay lại b() rồi mới đến a()?" Hãy giải thích trong 2 phút, dùng khái niệm Execution Context Stack.

Mô phỏng
  • Bạn nói Khi bạn gọi a(), JavaScript tạo ra một container (Execution Context) cho a và đẩy lên một cái stack — đó là cấu trúc chồng đĩa, đĩa nào vào sau thì lấy ra trước. Stack lúc này có Global ở đáy và a ở trên. Khi a() gọi b(), a không xong đâu — nó bị tạm dừng ở dòng gọi b(), và context của b được đẩy lên trên a. Tương tự, c được đẩy lên trên b. Khi c chạy xong, c bị lấy ra khỏi đỉnh stack. Lúc này đỉnh stack là b, nên b tiếp tục từ chỗ nó dừng lại. b xong thì lấy b ra, đỉnh stack là a, a tiếp tục. Không cần JavaScript "nhớ" đặc biệt — chính cấu trúc stack LIFO đã đảm bảo thứ tự quay lại đúng.

💡 Hình dung như xếp hộp đồ: bạn để hộp A xuống sàn, đặt hộp B lên trên A, đặt hộp C lên trên B. Khi mở C lấy đồ xong, bạn đóng C và tự động thấy B ngay dưới. B xong thì thấy A. Không cần nhãn mác — thứ tự xếp đã quyết định thứ tự mở.

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

  • Đồng nghiệp có hiểu tại sao a() không bị xóa không? Họ có hỏi "vậy bao nhiêu hộp có thể xếp chồng?" không?
  • Bạn có dự đoán được câu hỏi tiếp: "Nếu c() gọi lại a() thì sao?" không? (Trả lời: Stack sâu hơn, có thể overflow — nhưng đó là recursion, Module 1.5.)
  • Bạn có thể liên hệ đến DevTools stack trace không?

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giáNội dung
Giải thích LIFO stackExplain (Giải thích)Dùng lời riêng giải thích push/pop và paused
Trace push/pop sequenceTrace (Truy vết)Cho code 3 lớp lồng nhau, viết stack sau mỗi bước
Dự đoán số context đồng thờiPrediction (Dự đoán)Cho code nested, đoán số context tại điểm sâu nhất
Giải thích stack traceExplain (Giải thích)Cho 1 stack trace lỗi, giải thích tại sao có nhiều dòng

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

  • [ ] Có thể giải thích Execution Context Stack theo cơ chế LIFO bằng lời của chính mình
  • [ ] Có thể trace đúng thứ tự push và pop cho code có 3 lớp function lồng nhau (≥ 4/5 bước đúng)
  • [ ] Có thể dự đoán đúng số context đồng thời tồn tại tại điểm sâu nhất của stack
  • [ ] Có thể giải thích tại sao hàm ngoài bị paused (chứ không phải bị xóa) khi hàm trong chạy
  • [ ] Có thể đọc một stack trace đơn giản và giải thích ý nghĩa của mỗi dòng

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

Previous (Trước): Lesson 1.1.1–1.1.4 — Bạn đã biết Execution Context là container, Global FEC luôn tồn tại, Function FEC được tạo mỗi khi gọi hàm, và mỗi context có Creation Phase trước Execution Phase.

Current (Hiện tại): Bạn hiểu các context được xếp chồng lên nhau thành một stack LIFO. Khi hàm con chạy, hàm cha bị paused chứ không biến mất. Khi hàm con return, nó bị pop và hàm cha resume từ đúng chỗ pause.

Next (Tiếp theo):

  • 1.2.x — Scope & Lexical Environment (các context trên stack liên kết với nhau qua outer reference để tìm biến)
  • 1.3.x — Hoisting & TDZ (Creation Phase behavior trên từng context trong stack)
  • 1.5.x — Call Stack & Execution Tracing (stack frames, recursion, stack overflow, exception unwinding)
  • S3 — Async JavaScript (callbacks và Promise không push lên sync Execution Context Stack — đây là điểm then chốt phân biệt sync và async)
  • S8 — React re-render (mỗi lần render tạo một function call mới, stack mới, context mới)
📴 Offline Mode — Content served from cache