Lesson 1.1.3 — Function Execution Context
0. Metadata (Thông tin bài học)
| Trường | Giá trị |
|---|---|
| Stage | 1 — JavaScript Execution Model |
| Module | 1.1 — Execution Context |
| Lesson | 1.1.3 — Function Execution Context |
| Competency | C02 — JavaScript Runtime |
| Depth | L2–L3 (Explain → Use) |
| Prerequisites | Lesson 1.1.1, 1.1.2 — Execution Context & Global Execution Context |
| Thời gian ước tính | 30–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:
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ể:
- 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
- Dự đoán số lượng Function Execution Context được tạo ra trong một đoạn code
- Phân biệt local bindings (tham số + biến khai báo trong hàm) và global bindings
- 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/varbê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
returnhoặ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.
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)
thisbinding (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:
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
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ấtBướ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:
function counter() {
let count = 0;
count = count + 1;
console.log(count);
}
counter();
counter();
counter();Câu hỏi:
- Output của 3 lần
console.loglà gì? - Có bao nhiêu Function Execution Context được tạo ra?
- 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ĩ
- Output:
1,1,1 - 3 Function Execution Context (mỗi lần gọi
counter()một cái) - Vì
countlà local binding trong FEC. Mỗi lần gọi tạo FEC mới,countđược khởi tạo lại thành0, rồi tăng lên1. Giá trị không "nhớ" được giữa các lần gọi.
- Output:
Giải thích
- Lần gọi 1: FEC #1 →
count = 0→count = 1→ log1→ FEC #1 hủy →countbiến mất - Lần gọi 2: FEC #2 →
count = 0→count = 1→ log1→ FEC #2 hủy - Lần gọi 3: FEC #3 →
count = 0→count = 1→ log1→ FEC #3 hủy - Để
countnhớ đượ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).
- Lần gọi 1: FEC #1 →
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:
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 ý
- Global Context tạo đầu tiên
main()được gọi → tạo FEC chomain- Bên trong
main,greet("An")được gọi → tạo FEC chogreet greetreturn → hủy FECgreetgreet("Bình")được gọi → tạo FECgreetmớimainreturn → hủy FECmain
[Đáp án tham khảo]
Thứ tự tạo
- Global Execution Context
- FEC:
main() - FEC:
greet("An") - FEC:
greet("Bình")
Thứ tự hủy
- FEC:
greet("An")(sau khi return) - FEC:
greet("Bình")(sau khi return) - FEC:
main()(sau khiconsole.logxong) - Global Execution Context (khi script kết thúc)
- FEC:
Lưu ý Hai lần gọi
greet()tạo ra hai FEC hoàn toàn riêng biệt.namevàmessagetrong 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]
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ớiitems = []hoàn toàn mới. Array cũ từ lần gọi trước đã bị hủy cùng FEC cũ. Vì vậyitems.lengthluô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
function a() {
b();
}
function b() {
console.log("in b");
}
a();- What fails? Learner thường nghĩ khi
b()chạy, context củaa()đã biến mất - Why? Không. Khi
b()chạy, FEC củaa()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
function compute() {
const secret = 42;
return secret;
}
const result = compute();
console.log(secret);- What fails?
ReferenceError: secret is not defined - Why?
secretchỉ tồn tại trong FEC củacompute(). Khicompute()return, FEC bị hủy. Giá trị42được copy ra ngoài và gán choresult, nhưng biếnsecretthì 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ảireturnnó 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):
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
totalvà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
- 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?"
- 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?"
- 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?
- 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 FEC | Explain (Giải thích) | Liệt kê 4 bước: gọi → tạo → chạy → hủy |
| Dự đoán số lượng context | Prediction (Dự đoán) | Cho code có 3 lời gọi hàm, đoán số FEC |
| Phân biệt local/global | Classification (Phân loại) | Cho 5 khai báo, xác định cái nào thuộc FEC |
| Trace luồng dữ liệu | Trace (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)