Lesson 1.2.8 — Scope vs Execution Context (Phạm vi vs Ngữ cảnh Thực thi)
0. Metadata
| Trường | Giá trị |
|---|---|
| Stage | 1 — JavaScript Execution Model |
| Module | 1.2 — Scope & Lexical Environment |
| Lesson | 1.2.8 |
| Competency | C02 — JavaScript Runtime |
| Depth | L2–L3 (Explain → Use) |
| Prerequisites | Module 1.1 (Execution Context), Lesson 1.2.1–1.2.7 (Scope, Variable Resolution, Lexical Environment) |
| Cognitive Load | Cao |
1. Why This Exists (Vì sao cần học)
Bạn đang debug một hàm được gọi hai lần:
function counter() {
let count = 0;
count++;
console.log(count);
}
counter();
counter();Console in ra 1 rồi 1. Một junior dev hỏi: "Tại sao count không tăng lên 2? Cùng một function, cùng scope, sao biến không giữ lại giá trị?"
Câu trả lời nằm ở sự phân biệt then chốt:
Scope là "blueprint" (bản thiết kế). Execution Context là "công trường" (phiên xây dựng).
Mỗi lần gọi counter() tạo ra một công trường mới dựa trên cùng một bản thiết kế. Biến count được tạo mới trên mỗi công trường, không chia sẻ giữa các lần gọi.
Production Risk
Khi developer nhầm lẫn scope và execution context, họ:
- Tưởng rằng biến local "dùng chung" giữa các lần gọi function.
- Dùng "scope" và "execution context" thay thế cho nhau trong code review, gây hiểu nhầm.
- Không hiểu tại sao closure "giữ lại" được giá trị (vì họ chưa phân biệt scope structure và EC instance).
2. Prerequisites (Yêu cầu đầu vào)
Bạn cần đã biết:
- Execution Context là gì, creation phase vs execution phase (Module 1.1).
- Scope là vùng nhìn thấy, scope chain (Lesson 1.2.1, 1.2.5).
- Lexical Environment gồm Environment Record và Outer Reference (Lesson 1.2.7).
3. Learning Objectives (Mục tiêu học tập)
Sau lesson này, bạn có thể:
- Phân biệt Scope (static visibility) và Execution Context (runtime instance).
- Giải thích Lexical Environment là cấu trúc nối giữa scope và execution context.
- Dự đoán một biến local có được chia sẻ giữa các lần gọi function hay không.
- Phân biệt Scope Chain (tìm biến) và Call Stack (thứ tự thực thi).
- Không dùng "scope" và "execution context" thay thế cho nhau trong giải thích.
4. Mental Model (Mô hình tư duy)
4 Khái niệm, 4 Câu hỏi
| Khái niệm | Trả lời câu hỏi | Tính chất |
|---|---|---|
| Scope | Variable có thể được nhìn thấy từ đâu? | Static (xác định lúc viết code) |
| Execution Context | Code hiện tại đang được thực thi trong context nào? | Dynamic (tạo ra lúc chạy) |
| Lexical Environment | Environment lưu bindings + liên kết outer | Structure (scope) + Instance (EC) |
| Call Stack | Context nào đang thực thi trước context nào? | Dynamic (thứ tự runtime) |
Analogy:
- Scope = Bản thiết kế nhà (blueprint). Nói rõ phòng nào nhìn thấy phòng nào. Không đổi dù xây 1 hay 100 căn.
- Execution Context = Công trường xây nhà. Mỗi lần xây một căn = một EC. Có vật liệu riêng (bindings), có quy trình riêng (creation → execution).
- Lexical Environment = Danh sách vật liệu + bản đồ liên kết phòng. Tồn tại trong mỗi EC nhưng cấu trúc được vẽ từ blueprint.
- Call Stack = Thứ tự thi công. Căn này đang xây dở, rồi đến căn kia, rồi quay lại.
Sai lầm phổ biến
"Scope và Execution Context là một."
Sai. Một function có một scope (blueprint) nhưng có thể tạo ra vô số execution contexts (mỗi lần gọi một cái). Scope quyết định nhìn thấy gì. Execution context quyết định đang chạy cái gì, với giá trị nào, ở thời điểm nào.
5. Core Concepts (Các khái niệm cốt lõi)
| Loại | Nội dung |
|---|---|
| Essential (Bắt buộc) | Scope (static), Execution Context (dynamic), Lexical Environment (bridge), Call Stack (order) |
| Supporting (Hỗ trợ) | Mỗi EC có một lexical environment instance; cùng scope có thể có nhiều EC |
| Awareness (Biết tồn tại) | this binding thuộc EC, không thuộc scope (Stage 2) |
| Out of Scope (Không học ở đây) | Closure mechanism đầy đủ (Module 1.4), this deep dive (Stage 2), V8 internals (Stage 11) |
6. Worked Example (Ví dụ phân tích từng bước)
Input:
const rate = 1.1;
function compute(base) {
const tax = 0.1;
return base * (1 + tax) * rate;
}
compute(100);
compute(200);Step 1 — Observation: compute được gọi 2 lần. Cùng một function, cùng scope.
Step 2 — Classification: Cần phân biệt scope (một) và execution contexts (hai).
Step 3 — Reasoning:
BLUEPRINT (Scope — 1 cái duy nhất)
==================================
compute: parameters [base], locals [tax], outer [global]
CÔNG TRƯỜNG 1 (EC — lần gọi thứ nhất)
======================================
Lexical Environment Instance
├── base = 100
├── tax = 0.1
└── outer → global
Result: 100 * 1.1 * 1.1 = 121
CÔNG TRƯỜNG 2 (EC — lần gọi thứ hai)
======================================
Lexical Environment Instance
├── base = 200
├── tax = 0.1
└── outer → global
Result: 200 * 1.1 * 1.1 = 242Step 4 — Conclusion: Hai lần gọi tạo ra hai EC độc lập. Mỗi EC có lexical environment instance riêng chứa base và tax. Scope (blueprint) giống nhau, nhưng giá trị cụ thể (instance) khác nhau. tax được tạo mới trên mỗi EC, không chia sẻ.
7. Prediction Exercise (Bài tập dự đoán)
Đừng chạy code. Đoán output hoặc lỗi, sau đó giải thích tại sao.
P1 — Scope giống nhau, EC khác nhau
function ticket() {
let number = 0;
number++;
return number;
}
console.log(ticket());
console.log(ticket());[Đáp án & Giải thích]
- Bạn nghĩ: In ra
1rồi1. Mỗi lần gọiticket()tạo một execution context mới. Trong mỗi EC,numberđược khởi tạo là0rồi tăng lên1. Cùng scope (blueprint) nhưng khác EC (công trường), nên biến không chia sẻ. - Giải thích: Scope quyết định
numberlà local và chỉ nhìn thấy trongticket. Nhưng execution context quyết định mỗi lần gọi có mộtnumberriêng biệt.
P2 — Scope Chain vs Call Stack
function a() {
const x = "a";
b();
console.log(x);
}
function b() {
const x = "b";
c();
}
function c() {
console.log(x);
}
const x = "global";
a();[Đáp án & Giải thích]
- Bạn nghĩ: In ra
"global"rồi"a".c()consolex—cđược viết ở global scope, nên scope chain củaclàc → global. Nó tìm thấyx = "global". Call stack lúc đó làglobal → a → b → c, nhưng call stack không ảnh hưởng scope chain. Sau đóa()consolex—atìm thấyx = "a"local. - Giải thích: Đây là điểm then chốt: scope chain (lexical) ≠ call stack (runtime).
cđược gọi từb, nhưngckhông nhìn thấyxcủabvìcđược viết ở global. Đừng nhầm "được gọi từ đâu" với "nhìn thấy gì".
P3 — EC pop nhưng Lexical Environment vẫn còn (Preview Closure)
function makeCounter() {
let count = 0;
function increment() {
count++;
console.log(count);
}
return increment;
}
const counter = makeCounter();
counter();
counter();[Đáp án & Giải thích]
- Bạn nghĩ: In ra
1rồi2.makeCounter()tạo một EC, bên trong cócount = 0vàincrement.incrementcó outer reference trỏ về lexical environment củamakeCounter. KhimakeCounterreturn, EC bị pop khỏi call stack, nhưng lexical environment vẫn được giữ lại trong memory vìcounter(tứcincrement) vẫn trỏ đến nó. Mỗi lần gọicounter(), một EC mới choincrementđược tạo, nhưng outer reference vẫn trỏ về cùng một lexical environment cũ — nêncountđược chia sẻ giữa các lần gọi. - Giải thích: Đây là sự khác biệt giữa EC (tạo mới mỗi lần gọi) và lexical environment instance (có thể tồn tại lâu hơn EC nếu được reference giữ lại). Đây chính là mechanism của closure (Module 1.4).
8. Implementation Lab (Bài lab thực hành)
Lab: Trace EC + Scope Together (Guided → Independent)
Yêu cầu: Cho đoạn code sau, hãy mô tả:
- Có bao nhiêu scope (blueprint)?
- Có bao nhiêu execution context được tạo ra?
- Vẽ call stack tại thời điểm
console.log(a, b, c). - Vẽ scope chain của
console.log(a, b, c).
const a = "global-a";
function outer() {
const b = "outer-b";
function inner() {
const c = "inner-c";
console.log(a, b, c);
}
inner();
}
outer();[Đáp án tham khảo]
Bạn nghĩ:
- Scope (blueprint): 3 — Global,
outer,inner. - Execution contexts created: 3 — Global EC,
outer()EC,inner()EC. - Call stack tại
console.log:global → outer → inner(inner đang chạy ở top). - Scope chain của
console.log:inner → outer → global(lexical, không đổi).
Identifier Scope Chain Path Value cinner (local) "inner-c"binner → outer "outer-b"ainner → outer → global "global-a"- Scope (blueprint): 3 — Global,
Giải thích: Call stack và scope chain là hai cấu trúc khác nhau. Call stack nói "ai đang chạy". Scope chain nói "biến nào nhìn thấy". Trong ví dụ này chúng trùng hướng, nhưng P2 đã chứng minh chúng có thể khác nhau hoàn toàn.
9. Edge Cases (Các trường hợp ngoại lệ)
EC-1: this thuộc EC, không thuộc Scope
const obj = {
name: "Alice",
greet: function () {
console.log(this.name);
},
};
const fn = obj.greet;
fn();What fails? Nếu nghĩ this được resolve theo scope chain (lexical), bạn sẽ bối rối khi fn() in ra undefined (hoặc lỗi) thay vì "Alice".
Why? this là binding của execution context, không phải identifier được resolve theo scope chain. Khi obj.greet() được gọi, this là obj. Khi fn() được gọi (detached), this là global object (non-strict browser) hoặc undefined (strict mode / Node.js). Điều này thuộc Stage 2 (Object Model & this).
EC-2: eval tạo EC nhưng không tạo Scope mới
const x = 1;
function test() {
const x = 2;
eval("console.log(x); var y = 3;");
console.log(typeof y);
}
test();What fails? eval chạy trong EC hiện tại. var y = 3 bên trong eval vẫn thuộc function scope của test, không phải block scope mới.
Why? eval chạy code trong current lexical environment của nó và có thể tạo binding mới trong environment đó. var y bên trong eval vẫn thuộc function scope của test, không phải block scope mới. Trong strict mode, eval vẫn hoạt động nhưng tạo lexical environment riêng để cô lập binding, tránh leak ra outer scope. Vì lý do này, eval được khuyến cáo tránh dùng trong production.
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng): Developer báo cáo "biến bị reset" khi gọi hàm nhiều lần. Họ mong đợi biến local giữ lại giá trị giữa các lần gọi.
Reproduction (Tái hiện lỗi):
function accumulate(value) {
let total = 0;
total += value;
return total;
}
console.log(accumulate(5));
console.log(accumulate(10));Evidence (Bằng chứng): Console in ra 5 rồi 10, không phải 5 rồi 15.
Hypothesis (Giả thuyết): Developer tưởng rằng vì total được khai báo trong accumulate, và accumulate có "cùng một scope", nên total sẽ được chia sẻ giữa các lần gọi.
Verification (Xác minh): Thêm console.log trong accumulate để xác nhận total luôn bắt đầu từ 0.
Root Cause (Nguyên nhân gốc rễ): Nhầm lẫn Scope và Execution Context. Developer nghĩ "cùng scope = cùng biến". Thực tế, mỗi lần gọi accumulate() tạo một execution context mới, và mỗi EC có lexical environment instance riêng chứa total = 0. Scope (blueprint) chỉ nói rằng total là local — nó không nói rằng total được chia sẻ giữa các lần gọi.
Fix (Sửa): Nếu cần giữ lại state giữa các lần gọi, dùng closure (Module 1.4) hoặc global/module variable:
function createAccumulator() {
let total = 0;
return function (value) {
total += value;
return total;
};
}
const accumulate = createAccumulator();
console.log(accumulate(5)); // 5
console.log(accumulate(10)); // 15Prevention (Phòng ngừa): Khi thấy biến local "bị reset", đừng nghĩ scope sai. Hãy hỏi: "Tôi có cần giữ lại state giữa các lần gọi không?" Nếu có, bạn cần mechanism khác (closure, object, global) — không phải "sửa scope".
11. Design Exercise (Bài tập thiết kế)
Không áp dụng ở depth L2–L3. Design Exercise sẽ xuất hiện từ các lesson L5–L6 trở đi.
12. Production Scenario (Tình huống thực tế)
Context: Một hệ thống xử lý request HTTP:
function handleRequest(requestId) {
const startTime = Date.now();
function logDuration() {
const duration = Date.now() - startTime;
console.log(`Request ${requestId}: ${duration}ms`);
}
processData(logDuration);
}
handleRequest("A");
handleRequest("B");Symptom: Khi request B bắt đầu, developer thấy startTime của A vẫn tồn tại trong memory (qua heap snapshot). Họ nghi ngờ "scope bị leak".
Decision: Developer cần phân biệt: mỗi handleRequest() tạo một EC riêng với lexical environment chứa startTime và logDuration. Nếu processData giữ lại logDuration (ví dụ: callback async), lexical environment của handleRequest không được garbage collect dù EC đã pop. Đây không phải "scope bị leak" mà là closure retaining environment — behavior đúng nhưng cần quản lý.
Fix: Đảm bảo callback được cleanup sau khi chạy:
function handleRequest(requestId) {
const startTime = Date.now();
function logDuration() {
const duration = Date.now() - startTime;
console.log(`Request ${requestId}: ${duration}ms`);
}
processData(logDuration, () => {
// cleanup reference if possible
});
}💡 Trade-off: Closure là feature mạnh nhưng cần awareness về memory retention. Trong production, hãy đảm bảo callbacks không được giữ lại vô thời hạn (ví dụ: event listeners không remove). Chi tiết thuộc Stage 11 (Memory).
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: "Scope khác Execution Context như thế nào? Cùng một function gọi 2 lần tạo ra bao nhiêu scope và bao nhiêu execution context?"
- Hỏi AI:
What is the difference between scope and execution context in JavaScript? If I call the same function twice, how many scopes and execution contexts are created? - So sánh: AI có nói "each function call creates a new scope" không? Nếu có, đó là điểm mù.
- Verify: Đối chiếu với ECMAScript Spec — Execution Contexts hoặc MDN — JavaScript Execution.
Gợi ý
Để ý xem AI có nhầm:
- "Each function call creates a new scope" (sai: tạo EC mới, không tạo scope mới; scope là static).
- "Scope determines the order of execution" (sai: đó là call stack).
- "Execution context is the same as lexical environment" (sai: lexical environment là một phần của EC).
Đáp án tham khảo
- Bạn nghĩ: Scope là static structure (blueprint) xác định visibility. Cùng một function chỉ có một scope. Execution context là runtime instance (công trường) — mỗi lần gọi tạo một EC mới. Gọi function 2 lần = 1 scope + 2 ECs.
- AI trả lời: Thường trả lời đúng rằng scope là "where variables are accessible" và execution context là "the environment where code runs", nhưng hay dùng từ mơ hồ và đồng nhất hóa hai khái niệm.
- So sánh: AI thường không phân biệt rõ: scope là property của source code (1 lần viết = 1 scope), EC là property của runtime (N lần gọi = N ECs). AI cũng hay nói "function scope is created when function is called" — điều này nhầm lẫn scope structure với EC instance.
- Điểm AI nói sai hoặc quá mơ hồ: AI hay nói "when a function is called, a new scope is created" — scope (visibility structure) không được "tạo" khi gọi; nó là static property của source code, với outer reference được khóa tại function creation time. Điều được tạo khi gọi là lexical environment instance bên trong EC.
- Kết luận: Nếu bạn chỉ ra được scope là một (static blueprint) và EC là nhiều (runtime instances), bạn đã hiểu sâu hơn AI. Điều này là prerequisite cho closure (Module 1.4) và memory debugging (Stage 11).
14. Teach Back (Dạy lại)
Yêu cầu: Giải thích cho một junior dev trong 2 phút:
"Tại sao gọi
ticket()hai lần đều in ra1? Tại sao không phải do 'scope bị reset'?"
function ticket() {
let number = 0;
number++;
return number;
}
console.log(ticket());
console.log(ticket());Dùng đúng terminology: scope, execution context, lexical environment instance, blueprint.
Mô phỏng
- Bạn nói:
ticketcó một scope duy nhất — đó là blueprint xác địnhnumberlà biến local và chỉ nhìn thấy trongticket. Nhưng mỗi lần gọiticket(), JavaScript tạo một execution context mới. Trong mỗi EC có một lexical environment instance riêng, và trong đónumberđược khởi tạo là0, rồi tăng lên1. Lần gọi thứ hai tạo EC hoàn toàn mới, vớinumber = 0mới. Không có "scope bị reset" — scope không đổi. Đơn giản là mỗi EC có instance riêng. Nếu muốnnumbergiữ lại giá trị, ta cần closure: tạo một EC cha, để EC con (lần gọi sau) chia sẻ lexical environment với EC con (lần gọi trước).
💡 Hình dung như in vé số: cùng một máy in (scope — blueprint) nhưng mỗi lần bấm nút là một phiên in mới (EC — instance). Máy luôn bắt đầu đếm từ 0 mỗi phiên vì mỗi EC có lexical environment instance riêng. Nếu muốn đếm liên tục, bạn cần một cuốn sổ ghi chép bên ngoài máy in (closure giữ lexical environment cũ).
Gợi ý đánh giá bản thân
- Đồng nghiệp có hiểu tại sao không phải "scope bị reset" không?
- Bạn có thể dự đoán được nếu đổi
letthànhvarthì kết quả thay đổi không? (Gợi ý: không đổi — vẫn là local trong mỗi EC.) - Nếu họ hỏi "vậy làm sao để giữ lại
numbergiữa các lần gọi", bạn trả lời được 2 cách không? (Closure hoặc global variable.)
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Nội dung |
|---|---|---|
| Phân biệt Scope (static) và Execution Context (dynamic) | Explain (Giải thích) | Dùng lời riêng và analogy blueprint/công trường |
| Giải thích Lexical Environment là cầu nối | Prediction (Dự đoán) | Mô tả lexical environment thuộc EC nào và link đến scope nào |
| Dự đoán biến local có chia sẻ giữa các lần gọi | Prediction Exercise (P1–P3) | Cho function gọi nhiều lần, đoán output và giải thích bằng EC instance |
| Phân biệt Scope Chain và Call Stack | Prediction Exercise (P2) | Giải thích tại sao c() nhìn thấy global x dù được gọi từ b() (scope chain lexical ≠ call stack runtime) |
| Không dùng "scope" và "execution context" thay thế nhau | Teach Back | Giải thích đúng terminology trong 2 phút |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể phân biệt Scope (1 blueprint) và Execution Context (N instances khi gọi N lần).
- [ ] Có thể giải thích Lexical Environment là cấu trúc bên trong mỗi EC, link đến outer scope.
- [ ] Dự đoán đúng 3/3 scenarios trong Prediction Exercise.
- [ ] Có thể vẽ đồng thời call stack và scope chain cho cùng một đoạn code.
- [ ] Không dùng "scope" và "execution context" thay thế cho nhau trong giải thích.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Lexical Environment (Lesson 1.2.7) — hiểu cấu trúc environment record + outer reference.
Current (Hiện tại): Scope vs Execution Context — tổng hợp và phân biệt 4 khái niệm then chốt của Module 1.1 và 1.2.
Next (Tiếp theo):
- Module 1.3 — Hoisting & Temporal Dead Zone (tại sao biến tồn tại trong lexical environment nhưng vẫn không dùng được)
- Module 1.4 — Closure (lexical environment instance tồn tại sau khi EC pop)
- Module 1.5 — Call Stack & Execution Tracing (call stack deep dive)
- Stage 2 — Object Model &
this(thisbinding thuộc EC, không thuộc scope)- Stage 8 — React (re-render tạo EC mới, hooks closure giữ lexical environment cũ)
- Stage 11 — Memory (lexical environment retention sau khi EC pop)