Skip to content

Lesson 1.1.3 — Function Execution Context ​

Bài 1.1.3 — Function Execution Context
Function Calls, Local Execution & Context Lifecycle • 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.3 — Function Execution Context
CompetencyC02 — JavaScript Runtime
DepthL2–L3 (Explain → Use)
PrerequisitesLesson 1.1.1, 1.1.2 — Execution Context & Global Execution Context
Thời gian ước tính30–40 phút

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

Bạn đã biết Global Execution Context là container gốc. Nhưng phần lớn code JavaScript không chạy ở global — nó chạy bên trong hàm.

Xem đoạn code sau:

js
function trackVisit(userId) {
  const timestamp = Date.now();
  const log = `${userId} visited at ${timestamp}`;
  console.log(log);
  return log;
}

trackVisit("A001");
trackVisit("A002");

Nếu bạn nghĩ rằng timestamp từ lần gọi đầu tiên "còn lại" để lần gọi thứ hai dùng, bạn sẽ hiểu sai hoàn toàn. Nếu bạn nghĩ userId của hai lần gọi bị trộn lẫn, bạn sẽ viết code defensive không cần thiết.

Câu hỏi trung tâm của bài này:

Mỗi lần gọi hàm, JavaScript tạo ra và hủy bỏ cái gì?

Nếu không hiểu Function Execution Context, bạn sẽ:

  • Không hiểu tại sao biến local "biến mất" sau return
  • Không hiểu tại sao hai lần gọi cùng một hàm lại hoàn toàn độc lập
  • Không thể trace code có hàm lồng nhau
  • Không thể học Closure (Module 1.4) vì Closure bắt đầu từ việc hiểu context của hàm bên trong

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
  • Biết khai báo hàm, truyền tham số, return

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 điều gì xảy ra khi một hàm được gọi: tạo context, chạy code, return, hủy context
  2. Dự đoán số lượng Function Execution Context được tạo ra trong một đoạn code
  3. Phân biệt local bindings (tham số + biến khai báo trong hàm) và global bindings
  4. Trace luồng dữ liệu qua nhiều lần gọi hàm độc lập

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

Function Execution Context = Container tạm thời cho mỗi lần gọi hàm

Mỗi khi bạn viết foo(), engine tạo ra một Function Execution Context (FEC) mới. Container này:

  • Chứa các tham số (arguments) được truyền vào
  • Chứa các biến local khai báo bằng const/let/var bên trong hàm
  • Chỉ tồn tại trong khoảng thời gian hàm chạy
  • Bị xóa hoàn toàn khi hàm return hoặc kết thúc
  • Không chia sẻ biến local với các lần gọi khác của cùng một hàm

Đây là mental model chuẩn ở L2–L3. Ngoại lệ: Closure (Module 1.4) có thể giữ lại outer environment sau khi hàm return.

text
Gọi hàm
    ↓
┌─────────────────────────────┐
│  Tạo Function Execution     │
│  Context mới                │
│  ├── arguments: [1, 2]      │
│  ├── local bindings         │
│  └── outer reference        │
└───────────┬─────────────────┘
            ↓
    Chạy function body
            ↓
    return / kết thúc
            ↓
┌─────────────────────────────┐
│  Hủy Function Execution     │
│  Context                    │
│  (local bindings biến mất)  │
└─────────────────────────────┘

Quy tắc vàng

Mỗi lần gọi = một container mới. Gọi add(1,2) và add(10,20) tạo ra hai container hoàn toàn riêng biệt. result trong lần gọi thứ nhất không liên quan đến result trong lần gọi thứ hai.

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

Essential (Bắt buộc) ​

  • Function Execution Context (FEC): Context được tạo ra mỗi khi một hàm được gọi, chứa tham số và biến local
  • Arguments binding: Tham số truyền vào hàm trở thành binding trong FEC
  • Local bindings: Biến khai báo bên trong hàm chỉ tồn tại trong FEC của lần gọi đó
  • Context destruction: FEC bị xóa khi hàm return hoặc kết thúc, local bindings biến mất

Supporting (Hỗ trợ) ​

  • Outer reference: Mỗi FEC liên kết đến context bên ngoài (để tìm biến không có trong local)
  • Return value: Giá trị trả về được đưa ra ngoài trước khi context bị hủy

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

  • Nếu hàm return một hàm khác, inner function có thể giữ lại outer environment (đây là Closure, Module 1.4)

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

  • Creation Phase chi tiết (bài 1.1.4)
  • Scope Chain và Lexical Environment (Module 1.2)
  • Closure mechanism (Module 1.4)
  • this binding (Stage 2)
  • Recursion và Stack Overflow (Module 1.5)

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

Xem đoạn code sau:

js
function multiply(a, b) {
  const result = a * b;
  return result;
}

multiply(3, 4);
multiply(5, 6);

Bước 1 — Quan sát Hàm multiply được gọi hai lần với arguments khác nhau.

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

  • Lần gọi 1: multiply(3, 4) → tạo FEC #1
  • Lần gọi 2: multiply(5, 6) → tạo FEC #2

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

text
1. multiply(3, 4) được gọi
   → Tạo FEC #1
   → a = 3, b = 4 (arguments binding)
   → result = 12 (local binding)
   → return 12
   → FEC #1 bị hủy, result #1 biến mất

2. multiply(5, 6) được gọi
   → Tạo FEC #2 (hoàn toàn mới, không liên quan đến #1)
   → a = 5, b = 6
   → result = 30
   → return 30
   → FEC #2 bị hủy, result #2 biến mất

Bước 4 — Kết luận Hai lần gọi tạo ra hai FEC độc lập. Giá trị result không bị ghi đè vì chúng tồn tại trong hai container khác nhau, tại hai thời điểm khác nhau.

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

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

js
function counter() {
  let count = 0;
  count = count + 1;
  console.log(count);
}

counter();
counter();
counter();

Câu hỏi:

  1. Output của 3 lần console.log là gì?
  2. Có bao nhiêu Function Execution Context được tạo ra?
  3. Nếu developer mong đợi output là 1, 2, 3, tại sao kết quả lại khác?
[Đáp án & Giải thích]
  • Bạn nghĩ

    1. Output: 1, 1, 1
    2. 3 Function Execution Context (mỗi lần gọi counter() một cái)
    3. Vì count là local binding trong FEC. Mỗi lần gọi tạo FEC mới, count được khởi tạo lại thành 0, rồi tăng lên 1. Giá trị không "nhớ" được giữa các lần gọi.
  • Giải thích

    • Lần gọi 1: FEC #1 → count = 0 → count = 1 → log 1 → FEC #1 hủy → count biến mất
    • Lần gọi 2: FEC #2 → count = 0 → count = 1 → log 1 → FEC #2 hủy
    • Lần gọi 3: FEC #3 → count = 0 → count = 1 → log 1 → FEC #3 hủy
    • Để count nhớ được giữa các lần gọi, cần Closure (Module 1.4) hoặc global variable (không khuyến khích).

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

Lab 1 — Trace Context Creation (Guided) ​

Cho đoạn code sau. Hãy viết ra thứ tự tạo và hủy các Execution Context:

js
function greet(name) {
  const message = "Hello " + name;
  return message;
}

function main() {
  const a = greet("An");
  const b = greet("Bình");
  console.log(a, b);
}

main();

Gợi ý

  1. Global Context tạo đầu tiên
  2. main() được gọi → tạo FEC cho main
  3. Bên trong main, greet("An") được gọi → tạo FEC cho greet
  4. greet return → hủy FEC greet
  5. greet("Bình") được gọi → tạo FEC greet mới
  6. main return → hủy FEC main
[Đáp án tham khảo]
  • Thứ tự tạo

    1. Global Execution Context
    2. FEC: main()
    3. FEC: greet("An")
    4. FEC: greet("Bình")
  • Thứ tự hủy

    1. FEC: greet("An") (sau khi return)
    2. FEC: greet("Bình") (sau khi return)
    3. FEC: main() (sau khi console.log xong)
    4. Global Execution Context (khi script kết thúc)
  • Lưu ý Hai lần gọi greet() tạo ra hai FEC hoàn toàn riêng biệt. name và message trong lần gọi đầu không ảnh hưởng đến lần gọi sau.

Lab 2 — Isolation Proof (Independent) ​

Viết một hàm createBox(label) nhận vào một string, tạo một biến local items = [], push label vào items, rồi return items.length. Gọi hàm 3 lần với label khác nhau và chứng minh rằng mỗi lần gọi đều return 1 (chứng tỏ items không chia sẻ giữa các lần gọi).

[Đáp án tham khảo]
js
function createBox(label) {
  const items = [];
  items.push(label);
  return items.length;
}

console.log(createBox("A")); // 1
console.log(createBox("B")); // 1
console.log(createBox("C")); // 1
  • Giải thích Mỗi lần gọi createBox() tạo ra một FEC mới với items = [] hoàn toàn mới. Array cũ từ lần gọi trước đã bị hủy cùng FEC cũ. Vì vậy items.length luôn là 1.

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

Case 1 — Hàm gọi hàm tạo nhiều context đồng thời ​

js
function a() {
  b();
}

function b() {
  console.log("in b");
}

a();
  • What fails? Learner thường nghĩ khi b() chạy, context của a() đã biến mất
  • Why? Không. Khi b() chạy, FEC của a() vẫn tồn tại vì a() chưa return. Có hai context đồng thời tồn tại
  • How to observe? Tưởng tượng call stack: Global → a() → b(). Cả ba đều đang "mở"
  • How to fix/decide? Không cần fix. Nhưng cần hiểu để trace lỗi trong nested calls

Case 2 — Return không giữ lại context ​

js
function compute() {
  const secret = 42;
  return secret;
}

const result = compute();
console.log(secret);
  • What fails? ReferenceError: secret is not defined
  • Why? secret chỉ tồn tại trong FEC của compute(). Khi compute() return, FEC bị hủy. Giá trị 42 được copy ra ngoài và gán cho result, nhưng biến secret thì biến mất
  • How to observe? Chạy code và thấy lỗi ở dòng console.log(secret)
  • How to fix/decide? Nếu cần dùng secret ở ngoài, phải return nó hoặc khai báo ở scope rộng hơn

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

Symptom (Triệu chứng): Developer báo cáo: "Tôi có hàm accumulate() cộng dồn giá trị, nhưng mỗi lần gọi nó lại bắt đầu từ 0."

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

js
function accumulate(value) {
  let total = 0;
  total = total + value;
  return total;
}

console.log(accumulate(5)); // 5
console.log(accumulate(3)); // 3 (developer expects 8)

Evidence (Bằng chứng): Lần gọi thứ hai trả về 3 thay vì 8.

Hypothesis (Giả thuyết): Developer nhầm lẫn giữa local variable lifetime và persistent state. total là local binding trong FEC. Mỗi lần gọi tạo FEC mới, total được khởi tạo lại thành 0.

Verification (Xác minh): Thêm console.log(total) bên trong hàm trước khi return — thấy total luôn bắt đầu từ 0.

Root Cause (Nguyên nhân gốc rễ): Không hiểu Function Execution Context được tạo mới mỗi lần gọi và bị hủy sau khi return. Local bindings không tồn tại qua các lần gọi.

Prevention (Phòng ngừa):

  • Nếu cần state tồn tại qua nhiều lần gọi, dùng Closure (Module 1.4) hoặc global variable (cẩn thận pollution)
  • Nếu chỉ cần tính toán đơn thuần, truyền total vào như tham số: accumulate(total, value)

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 API endpoint xử lý thanh toán. Mỗi request tạo một transaction ID và log các bước xử lý.

Constraint: Không dùng global variable để lưu transaction ID vì server xử lý nhiều request đồng thời.

Symptom: Developer junior khai báo let txId ở global scope, rồi gán giá trị bên trong hàm xử lý request. Khi 2 request đến cùng lúc, transaction ID bị ghi đè lẫn nhau, log bị lẫn lộn.

Decision cần đưa ra: Dùng kiến thức về Function Execution Context, giải thích tại sao mỗi request nên tạo txId như một local binding bên trong hàm xử lý thay vì global variable.

Gợi ý

Mỗi request gọi hàm xử lý = một Function Execution Context mới. Nếu txId là local binding trong hàm, mỗi request có txId riêng biệt, không bao giờ xung đột. Nếu để txId ở Global Execution Context, tất cả request chia sẻ cùng một biến → race condition.

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: "Điều gì xảy ra khi tôi gọi một hàm? Tại sao biến local không tồn tại sau khi hàm return?"
  2. Hỏi AI: Dùng prompt: "What happens in JavaScript when a function is called? Does a function call create a new execution context? What happens to local variables after return?"
  3. So sánh: AI có nhắc đến việc mỗi lần gọi tạo context mới không? AI có giải thích tại sao hai lần gọi cùng hàm không chia sẻ local variables không?
  4. Verify: Mở MDN "Functions" hoặc ECMAScript spec, tìm "function call execution context". Kiểm chứng xem AI có bỏ sót điểm nào.

Gợi ý

Để ý xem AI có dùng từ mơ hồ như "variables are destroyed" không. Câu trả lời chính xác cần nói: "Function Execution Context is created on each call and removed after return." Nếu AI nói "local variables are deleted" mà không nhắc đến context, đó là dấu hiệu trả lời đúng kết quả nhưng sai mechanism.

Đáp án tham khảo
  • Bạn nghĩ Khi gọi hàm, engine tạo ra một Function Execution Context mới. Context này chứa tham số và biến local. Hàm chạy xong, context bị hủy, local variables biến mất theo. Hai lần gọi cùng hàm = hai context riêng biệt.

  • AI trả lời

    • "When a function is called, JavaScript creates a new execution context for that function."
    • "Local variables exist only within that function's scope and are destroyed when the function returns."
    • "Each function call gets its own set of local variables, so multiple calls don't interfere with each other."
    • "The return value is passed back to the caller, but the local execution context is cleaned up."
  • So sánh

    • AI đúng về cơ chế tạo context và hủy context.
    • AI dùng từ "scope" và "destroyed" — hơi mơ hồ. Trong curriculum này, chúng ta phân biệt rõ: biến không bị "destroyed" một cách chủ động; mà là context chứa biến bị hủy, kéo theo biến không còn reachable.
    • AI không nhắc đến arguments binding — một phần quan trọng của FEC.
  • Điểm AI nói sai hoặc quá mơ hồ

    • AI nói "variables are destroyed" — điều này đúng ở level high-level nhưng quá tuyệt đối. Nếu inner function giữ reference đến biến local (qua Closure — Module 1.4), biến đó vẫn còn tồn tại sau khi outer function return. Vì vậy "destroyed" không phải lúc nào cũng đúng.
    • AI thường không nhắc đến ngoại lệ này, dẫn đến learner nghĩ context luôn biến mất 100% sau return.
  • Kết luận Nếu bạn phát hiện AI dùng từ "destroyed" một cách quá đơn giản và bỏ sót trường hợp Closure, bạn đã hiểu rằng Function Execution Context không phải lúc nào cũng "biến mất hoàn toàn". Đây là nền tảng để học Closure ở Module 1.4, nơi context "sống lại" qua inner function.

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

Yêu cầu: Giả sử một junior developer hỏi: "Tôi gọi hàm calculate(5) hai lần. Tại sao biến temp bên trong hàm không nhớ giá trị từ lần gọi trước?" Hãy giải thích trong 2 phút, dùng khái niệm Function Execution Context.

Mô phỏng
  • Bạn nói Mỗi khi bạn gọi calculate(5), JavaScript tạo ra một Function Execution Context — một container tạm thời chỉ dành cho lần gọi đó. Bên trong container đó, temp được khai báo và tính toán. Khi hàm chạy xong và return, container này bị dẹp đi. Lần gọi thứ hai, engine tạo một container hoàn toàn mới, temp được khởi tạo lại từ đầu. Nó không biết container cũ từng tồn tại. Vì vậy hai lần gọi hoàn toàn độc lập — như thuê hai phòng khách sạn khác nhau, dọn sạch sau mỗi lần trả.

💡 Mỗi lần gọi hàm là nhận một phòng mới. Đồ để trong phòng lần trước đã bị dọn sạch khi bạn trả phòng. Nếu muốn đồ "theo" bạn qua nhiều lần thuê, bạn cần một cơ chế khác — đó là Closure, chúng ta sẽ học sau.

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

  • Đồng nghiệp có hiểu tại sao biến "mất" không? Họ có hỏi "vậy làm sao giữ lại?" không?
  • Bạn có dự đoán được câu hỏi tiếp: "Nếu tôi muốn nhớ giá trị giữa các lần gọi thì sao?" không?
  • Bạn có thể phân biệt được "return giá trị ra ngoài" và "biến local vẫn còn" không?

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giáNội dung
Giải thích vòng đời FECExplain (Giải thích)Liệt kê 4 bước: gọi → tạo → chạy → hủy
Dự đoán số lượng contextPrediction (Dự đoán)Cho code có 3 lời gọi hàm, đoán số FEC
Phân biệt local/globalClassification (Phân loại)Cho 5 khai báo, xác định cái nào thuộc FEC
Trace luồng dữ liệuTrace (Truy vết)Theo dõi arguments qua 2 lần gọi hàm độc lập

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

  • [ ] Có thể giải thích 4 bước vòng đời của Function Execution Context (tạo → chạy → return → hủy)
  • [ ] Có thể dự đoán đúng số FEC được tạo trong code có nhiều lời gọi hàm (≥ 3/4 scenario đúng)
  • [ ] Có thể phân biệt local bindings (tham số + biến trong hàm) và global bindings
  • [ ] Có thể giải thích tại sao hai lần gọi cùng hàm không chia sẻ biến local
  • [ ] Có thể trace luồng dữ liệu qua nested function calls 2 cấp

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

Previous (Trước): Lesson 1.1.1 & 1.1.2 — Bạn đã biết Execution Context là container, và Global Execution Context là container gốc chứa global object và global bindings.

Current (Hiện tại): Bạn hiểu Function Execution Context là container tạm thời tạo ra mỗi khi gọi hàm, chứa arguments và local bindings, và bị hủy khi hàm return.

Next (Tiếp theo):

  • 1.1.4 — Creation Phase vs Execution Phase (chi tiết bên trong mỗi context)
  • 1.1.5 — Execution Context Stack (nhiều context tồn tại đồng thời, push/pop)
  • 1.2.x — Scope & Lexical Environment (biến tìm nhau qua outer reference)
  • 1.4.x — Closure (khi inner function giữ lại outer context sau khi outer return)
  • S3 — Async callbacks (callback được gọi sau khi context gốc đã kết thúc)
  • S8 — React re-render (mỗi lần render = một function call = context mới)
📴 Offline Mode — Content served from cache