Lesson 1.2.1 — Scope là gì? (What Is Scope?)
0. Metadata
| Trường | Giá trị |
|---|---|
| Stage | 1 — JavaScript Execution Model |
| Module | 1.2 — Scope & Lexical Environment |
| Lesson | 1.2.1 |
| Competency | C02 — JavaScript Runtime |
| Depth | L2–L3 (Explain → Use) |
| Prerequisites | Execution Context (Module 1.1), Variables (const/let/var), Functions |
| Cognitive Load | Trung bình |
1. Why This Exists (Vì sao cần học)
Bạn viết hàm và khai báo một biến bên trong:
function authenticate() {
const token = "secret-123";
}
authenticate();
console.log(token);Production Risk
Chạy đoạn code trên sẽ ra ReferenceError: token is not defined. Nếu không hiểu tại sao, developer thường "fix" bằng cách bỏ const hoặc khai báo token ở global — tạo ra lỗ hổng bảo mật và global pollution.
Scope trả lời câu hỏi:
"Từ vị trí này trong code, tôi có thể nhìn thấy biến nào?"
Nếu bạn không nắm chắc câu trả lời, bạn sẽ không thể:
- Dự đoán được
ReferenceErrorhay không. - Hiểu Closure (Module 1.4).
- Debug được stale variable trong React (Stage 8).
- Tránh global pollution trong production.
2. Prerequisites (Yêu cầu đầu vào)
Bạn cần đã biết:
const,let,varkhai báo biến như thế nào.- Function được gọi ra sao.
- Execution Context là "môi trường code đang chạy" (Module 1.1).
Nếu chưa rõ Execution Context, quay lại Lesson 1.1.1–1.1.3 trước.
3. Learning Objectives (Mục tiêu học tập)
Sau lesson này, bạn có thể:
- Phân biệt Scope và Execution Context.
- Giải thích lexical scope bằng mental model "vùng nhìn thấy".
- Dự đoán một biến có accessible hay không từ một vị trí code cho trước.
- Nhận diện 3 điều mà scope KHÔNG PHẢI: lifetime, value, execution context.
- Vẽ/mô tả scope chain đơn giản cho code lồng nhau 2–3 tầng.
4. Mental Model (Mô hình tư duy)
Scope = Visibility Zone (Vùng nhìn thấy)
Scope không phải là "nơi biến sống" (lifetime), cũng không phải "giá trị của biến" (value). Scope là quy tắc xác định: từ vị trí này, identifier nào có thể được nhìn thấy.
Hãy hình dung code như một tòa nhà lồng nhau:
Tầng Trệt (Global)
└── Phòng A (Function)
└── Ngăn tủ A1 (Block)- Từ Ngăn tủ A1, bạn nhìn thấy đồ trong Ngăn tủ A1, Phòng A, và Tầng Trệt.
- Từ Tầng Trệt, bạn không nhìn thấy đồ trong Phòng A.
Đây chính là Lexical Scope (tĩnh): vùng nhìn thấy được xác định bởi vị trí code được viết, không phụ thuộc vào lúc chạy.
Sai lầm phổ biến
"Scope là nơi biến được tạo ra."
Sai. Biến có thể tồn tại trong memory (lifetime) nhưng không nhìn thấy được từ scope hiện tại. Scope chỉ quyết định visibility, không quyết định existence.
5. Core Concepts (Các khái niệm cốt lõi)
| Loại | Nội dung |
|---|---|
| Essential (Bắt buộc) | Scope, Visibility, Accessibility, Lexical Scope |
| Supporting (Hỗ trợ) | Scope Chain, Nested Scope |
| Awareness (Biết tồn tại) | Dynamic Scope (một số ngôn ngữ khác có, JS không dùng) |
| Out of Scope (Không học ở đây) | Hoisting (1.3), Closure (1.4), TDZ (1.3), Environment Record (1.2.7) |
Quy tắc vàng
JavaScript dùng Lexical Scope (static scope). Vùng nhìn thấy của một biến được khóa cứng tại thời điểm viết code, không thay đổi khi runtime.
6. Worked Example (Ví dụ phân tích từng bước)
Input:
const globalVar = "I am global";
function outer() {
const outerVar = "I am outer";
function inner() {
const innerVar = "I am inner";
console.log(globalVar);
console.log(outerVar);
console.log(innerVar);
}
inner();
}
outer();Step 1 — Observation: inner() được gọi. Engine cần resolve 3 identifier: globalVar, outerVar, innerVar.
Step 2 — Classification: Đây là variable resolution theo lexical scope.
Step 3 — Reasoning:
| Identifier | Tìm ở local inner | Tìm ở outer | Tìm ở Global | Kết quả |
|---|---|---|---|---|
innerVar | ✅ Tìm thấy | — | — | "I am inner" |
outerVar | ❌ Không có | ✅ Tìm thấy | — | "I am outer" |
globalVar | ❌ Không có | ❌ Không có | ✅ Tìm thấy | "I am global" |
Step 4 — Conclusion: Code chạy thành công vì scope chain cho phép tìm từ trong ra ngoài. Nếu đảo ngược (tìm từ ngoài vào trong), outerVar và innerVar sẽ không bao giờ thấy được.
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 — Basic
function foo() {
const x = 10;
}
console.log(x);[Đáp án & Giải thích]
- Bạn nghĩ:
ReferenceErrorvìxđược khai báo trong scope củafoo, không nhìn thấy từ global. - Giải thích: Scope của
foolà "phòng kín". Từ global (bên ngoài), engine không thể resolvex.
P2 — Nested Lookup
const a = 1;
function bar() {
console.log(a);
}
bar();[Đáp án & Giải thích]
- Bạn nghĩ: In ra
1.akhông có trongbar, nhưng scope chain cho phép nhìn ra ngoài đến global. - Giải thích: Lexical scope cho phép "nhìn từ trong ra ngoài", không phải "từ ngoài vào trong".
P3 — Misleading Shadow
let b = 2;
function baz() {
let b = 3;
console.log(b);
}
baz();
console.log(b);[Đáp án & Giải thích]
- Bạn nghĩ: In ra
3rồi2. Biếnbbên trongbaz"che" (shadow) biếnbbên ngoài, nhưng chỉ trong scope củabaz. - Giải thích: Đây là scope shadowing (sẽ học chi tiết ở Lesson 1.2.5). Mỗi scope có
briêng, không liên quan.
8. Implementation Lab (Bài lab thực hành)
Lab: Private Config (Guided → Independent)
Yêu cầu: Viết createApp sao cho:
configchỉ accessible từ bên trongcreateApp.connect()có thể đọcconfig.api.- Từ bên ngoài,
app.configphải làundefined.
Starter:
function createApp() {
// TODO: Khai báo config sao cho chỉ hàm bên trong mới thấy
const config = { api: "https://api.example.com" };
function connect() {
// TODO: Truy cập config tại đây
return config.api;
}
return { connect };
}
const app = createApp();
console.log(app.connect()); // "https://api.example.com"
console.log(app.config); // undefined[Đáp án tham khảo]
- Bạn nghĩ: Khai báo
configbên trongcreateApp, return chỉconnect. Vìconfignằm trong scope củacreateApp, từ global không thể truy cập trực tiếp. - Code đã đúng: Starter code đã hoàn thành yêu cầu. Điểm mấu chốt là
configkhông nằm trong object return. - Giải thích: Scope của
createApp"che chắn"config. Đây là nền tảng của private state bằng closure (sẽ học ở Module 1.4).
Lab Vẽ Scope Chain
Cho đoạn code sau. Hãy vẽ sơ đồ scope chain dạng cây, ghi rõ biến nào nhìn thấy được từ mỗi vị trí:
const global = "G";
function outer() {
const out = "O";
function inner() {
const inn = "I";
console.log(global, out, inn);
}
inner();
}
outer();[Đáp án tham khảo]
Bạn nghĩ: Khi
inner()chạyconsole.log(global, out, inn), JavaScript sẽ tìm từng biến theo thứ tự từ trong ra ngoài:innngay tronginner,outởouter, vàglobalở global scope.Sơ đồ Scope Chain (dạng cây):
Global Scope
├── const global = "G"
├── function outer()
│
└── [Outer Scope] — khi outer() được gọi
├── const out = "O"
├── function inner()
│
└── [Inner Scope] — khi inner() được gọi
├── const inn = "I"
└── console.log(global, out, inn)Từ vị trí console.log trong inner, JS engine nhìn thấy:
| Biến | Scope tìm thấy | Giá trị |
|---|---|---|
global | Global Scope (đi qua Outer Scope) | "G" |
out | Outer Scope | "O" |
inn | Inner Scope (local) | "I" |
Giải thích: Đây là cơ chế Lexical Scoping (tĩnh scope). Khi một function được định nghĩa, nó "nhớ" scope cha của mình và tạo thành một scope chain (chuỗi scope). Quy tắc tìm biến của JS engine luôn là:
- Tìm trong Local Scope của function hiện tại
- Không thấy → nhảy lên Outer Scope (scope của function bao quanh)
- Tiếp tục cho đến Global Scope
- Vẫn không thấy →
ReferenceError
Trong ví dụ này, scope chain là:
Inner Scope → Outer Scope → Global Scope. Nhờ vậyinner()có thể truy cập cả 3 biến mặc dù nó chỉ khai báoinn. Đây cũng chính là nền tảng của closure — function con giữ quyền truy cập vào scope của function cha ngay cả khi cha đã chạy xong.
9. Edge Cases (Các trường hợp ngoại lệ)
EC-1: Scope ≠ Execution Context
function fn() {
const x = Math.random();
console.log(x);
}
fn();
fn();What fails? Nếu nghĩ "mỗi lần gọi tạo scope mới", bạn sẽ nhầm scope với execution context.
Why? Mỗi lần gọi tạo ra Execution Context mới (có x khác nhau), nhưng scope (cấu trúc) vẫn giống nhau. Scope là static, EC là runtime instance.
EC-2: Scope ≠ Lifetime (Preview)
function outer() {
const secret = 42;
return function inner() {
return secret;
};
}
const getSecret = outer();
// outer() đã return, EC của outer đã pop khỏi stack
// nhưng `secret` vẫn accessible từ `getSecret()`Why? Biến secret vẫn tồn tại trong memory (lifetime kéo dài) nhờ closure, nhưng scope của nó vẫn là "bên trong outer". Đây là lesson 1.4.
10. Debug Lab (Bài lab gỡ lỗi)
Sai lầm phổ biến
Developer thường nhìn thấy ReferenceError và đoán "biến bị xóa" hoặc "chưa khai báo đúng cách".
Symptom (Triệu chứng): ReferenceError: userConfig is not defined
Reproduction (Tái hiện lỗi):
function loadConfig() {
const userConfig = { theme: "dark" };
}
loadConfig();
console.log(userConfig.theme);Evidence (Bằng chứng): Dòng lỗi trỏ vào console.log(userConfig.theme).
Hypothesis (Giả thuyết): userConfig được khai báo trong scope của loadConfig, nhưng console.log chạy ở global scope.
Verification (Xác minh): Di chuyển console.log vào trong loadConfig → lỗi biến mất.
Root Cause (Nguyên nhân gốc rễ): Scope mismatch. Identifier chỉ visible trong scope nơi nó được khai báo.
Fix (Sửa):
function loadConfig() {
const userConfig = { theme: "dark" };
return userConfig;
}
const config = loadConfig();
console.log(config.theme);Prevention (Phòng ngừa): Trước khi dùng biến, xác định scope của nó bằng cách nhìn vào vị trí khai báo trong source code, không phải vị trí chạy.
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 codebase có 20 file script được nối bằng <script> tag. Mỗi file đều khai báo:
var config = { apiUrl: "..." };Symptom: File load sau cùng ghi đè config của file trước. Ứng dụng crash ngẫu nhiên.
Decision: Developer cần nhận ra var config ở global scope tạo ra global pollution. Mỗi file đều "nhìn thấy" và có thể ghi đè biến của file khác.
Fix: Đưa config vào function scope (ví dụ: bọc trong IIFE hoặc hàm wrapper) để ẩn khỏi global. Module scope (ESM) cũng giải quyết triệt để vấn đề này nhưng thuộc Stage 7 — chúng ta sẽ học sau.
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: "Lexical scope khác execution context như thế nào?"
- Hỏi AI:
Explain the difference between scope and execution context in JavaScript. - So sánh: AI có dùng từ "where variables live" hoặc "scope is created at runtime" không?
- Verify: Đối chiếu với MDN — Scope.
Gợi ý
Để ý xem AI có nhầm:
- "Scope = lifetime" (sai: scope là visibility, lifetime là duration).
- "Scope thay đổi mỗi lần gọi function" (sai: lexical scope là static).
Đáp án tham khảo
- Bạn nghĩ: Lexical scope là cấu trúc tĩnh xác định biến nào nhìn thấy ở đâu. Execution context là runtime instance khi function chạy. Cùng một function gọi 100 lần tạo 100 EC, nhưng chỉ có 1 scope structure.
- AI trả lời: Thường đúng ở bề mặt, nhưng hay dùng từ mơ hồ như "scope determines the accessibility" (đúng) rồi nói "variables are looked up in the scope chain during execution" (có thể gây nhầm với dynamic scope).
- So sánh: AI thường không nhấn mạnh lexical = static = determined at author time. Đây là điểm then chốt để hiểu closure và debugging.
- Điểm AI nói sai hoặc quá mơ hồ: AI hay nói "scope is created when function is called" — thực tế scope structure được xác định khi viết code (parse time), chỉ có execution context mới được tạo khi gọi.
- Kết luận: Nếu bạn chỉ ra được scope là static structure còn EC là runtime instance, bạn đã hiểu sâu hơn AI. Điều này sẽ giúp bạn debug closure ở Module 1.4.
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 đoạn code này lỗi? Và tại sao không phải do biến bị xóa?"
function setup() {
const apiKey = "sk-123";
}
setup();
console.log(apiKey);Dùng đúng terminology: scope, visibility, lexical, global scope, function scope.
Mô phỏng
- Bạn nói:
apiKeyđược khai báo bên trong functionsetup, nghĩa là nó thuộc function scope củasetup. JavaScript dùng lexical scope, nghĩa là vùng nhìn thấy được quyết định bởi vị trí code được viết. Từ global scope (bên ngoàisetup), engine không thể nhìn thấy vào trong function. LỗiReferenceErrorkhông phải vì biến bị xóa, mà vì từ vị trí củaconsole.log,apiKeynằm ngoài visibility zone. Để fix, ta phải return giá trị ra hoặc di chuyểnconsole.logvào trongsetup.
💡 Hình dung scope như các phòng kính một chiều: từ trong phòng nhìn ra ngoài được, từ ngoài nhìn vào trong thì không.
Gợi ý đánh giá bản thân
- Đồng nghiệp có hiểu tại sao lỗi không phải do "biến bị xóa" không?
- Bạn có thể dự đoán được case tương tự với 3 tầng lồng nhau không?
- Nếu họ hỏi "scope khác execution context thế nào", bạn trả lời được không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Nội dung |
|---|---|---|
| Phân biệt Scope và Execution Context | Explain (Giải thích) | Dùng lời riêng giải thích scope (static) khác EC (runtime instance) |
| Giải thích lexical scope | Prediction (Dự đoán) + Teach Back | Dự đoán output dựa trên vị trí viết code, không phải thứ tự chạy |
| Dự đoán accessibility của biến | Prediction Exercise (P1–P3) | Cho 3 đoạn code nested, đoán biến nào visible từ vị trí cho trước |
| Nhận diện scope ≠ lifetime/value | Debug Lab | Giải thích root cause ReferenceError bằng scope mismatch, không phải biến bị xóa |
| Vẽ scope chain đơn giản | Implementation Lab | Cho code lồng nhau 2–3 tầng, vẽ/mô tả scope chain từ trong ra ngoài |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể giải thích scope là visibility zone, không phải lifetime hay value.
- [ ] Có thể phân biệt scope (static structure) với execution context (runtime instance).
- [ ] Dự đoán đúng 3/3 scenarios trong Prediction Exercise.
- [ ] Có thể trace scope chain từ trong ra ngoài cho code lồng nhau 2–3 tầng.
- [ ] Giải thích được tại sao
ReferenceErrorxảy ra trong Debug Lab bằng scope mismatch.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Execution Context — biết code chạy trong "môi trường" nào.
Current (Hiện tại): Scope — biết từ vị trí đó, biến nào nhìn thấy được.
Next (Tiếp theo):
- Lesson 1.2.2 — Global Scope
- Lesson 1.2.3 — Function Scope
- Lesson 1.2.4 — Block Scope
- Lesson 1.2.5 — Nested Scope & Shadowing
- Module 1.4 — Closure (scope + lifetime kết hợp)
- Stage 3 — Async callbacks giữ scope
- Stage 8 — React stale closure