Lesson 1.2.3 — Function Scope (Phạm vi Hàm)
0. Metadata
| Trường | Giá trị |
|---|---|
| Stage | 1 — JavaScript Execution Model |
| Module | 1.2 — Scope & Lexical Environment |
| Lesson | 1.2.3 |
| Competency | C02 — JavaScript Runtime |
| Depth | L2–L3 (Explain → Use) |
| Prerequisites | Lesson 1.2.1 — Scope là gì?, Lesson 1.2.2 — Global Scope |
| Cognitive Load | Trung bình |
1. Why This Exists (Vì sao cần học)
Bạn viết một hàm tiện ích để tính toán:
function calculateTotal() {
var tax = 0.1;
var discount = 5;
return price * (1 + tax) - discount;
}Sau đó bạn nhận ra price không được khai báo ở đâu cả, nhưng code vẫn chạy vì nó đọc price từ global. Hoặc ngược lại, bạn khai báo var tax = 0.1 và tưởng rằng bên ngoài hàm không thấy được.
Production Risk
Nếu không hiểu function scope, developer thường:
- Để lộ biến nội bộ ra ngoài (information leak).
- Dùng
vartưởng rằng nó block-scoped nhưlet, gây bug trong loop hoặc condition. - Không hiểu tại sao IIFE "che giấu" được biến.
Function scope trả lời câu hỏi:
"Những biến khai báo bên trong một hàm có thể nhìn thấy từ đâu?"
2. Prerequisites (Yêu cầu đầu vào)
Bạn cần đã biết:
- Scope là "vùng nhìn thấy" (Lesson 1.2.1).
- Global scope là scope ngoài cùng (Lesson 1.2.2).
var,let,constkhai báo biến.
3. Learning Objectives (Mục tiêu học tập)
Sau lesson này, bạn có thể:
- Giải thích function scope là scope được tạo ra khi hàm được định nghĩa.
- Phân biệt biến khai báo bên trong hàm (local) và biến bên ngoài (global/outer).
- Dự đoán một biến có accessible hay không từ bên ngoài hàm.
- Nhận diện sự khác biệt giữa
var(function-scoped) vàlet/const(block-scoped) ở mức preview. - Refactor code để encapsulate biến bằng function scope.
4. Mental Model (Mô hình tư duy)
Function Scope = Phòng Riêng (Private Room)
Hãy hình dung mỗi function là một phòng riêng trong tòa nhà:
Tầng Trệt (Global Scope)
└── Phòng A (Function Scope)
├── Bàn làm việc (local variables)
└── Cửa sổ nhìn ra tầng trệt (nhìn thấy outer scope)- Từ bên trong phòng, bạn nhìn thấy đồ trong phòng và đồ ở tầng trệt.
- Từ tầng trệt, bạn không nhìn thấy đồ trong phòng.
- Mỗi lần gọi hàm = mở một phiên làm việc mới trong phòng đó, nhưng cấu trúc phòng (scope) không đổi.
Sai lầm phổ biến
"Function scope chỉ tạo ra khi hàm được gọi."
Sai một nửa. Scope structure (cấu trúc vùng nhìn thấy) được xác định khi hàm được viết (lexical). Nhưng scope instance (các binding cụ thể) được tạo ra khi hàm được gọi (execution context). Đừng nhầm lẫn hai khái niệm này.
5. Core Concepts (Các khái niệm cốt lõi)
| Loại | Nội dung |
|---|---|
| Essential (Bắt buộc) | Function scope, Local variables, Outer scope visibility, Encapsulation |
| Supporting (Hỗ trợ) | var là function-scoped, let/const là block-scoped (preview) |
| Awareness (Biết tồn tại) | Function declaration tạo binding khác function expression |
| Out of Scope (Không học ở đây) | Hoisting (Module 1.3), Closure (Module 1.4), Execution context internals (Module 1.1) |
6. Worked Example (Ví dụ phân tích từng bước)
Input:
const globalRate = 1.2;
function calculate(price) {
const tax = 0.1;
const total = price * (1 + tax) * globalRate;
return total;
}
console.log(calculate(100));
console.log(typeof tax);Step 1 — Observation: calculate(100) được gọi. Bên trong calculate cần resolve price, tax, globalRate, total. Dòng console.log(typeof tax) chạy ở global scope.
Step 2 — Classification: Variable resolution theo lexical scope. tax là local variable của calculate.
Step 3 — Reasoning:
| Identifier | Local calculate | Outer/Global | Kết quả |
|---|---|---|---|
price | ✅ Tham số | — | 100 |
tax | ✅ Khai báo local | — | 0.1 |
globalRate | ❌ Không có | ✅ Global | 1.2 |
total | ✅ Khai báo local | — | 132 |
tax (từ global) | — | ❌ Không có | "undefined" |
Step 4 — Conclusion: calculate(100) trả về 132. typeof tax ở global trả về "undefined" vì tax thuộc function scope của calculate, không nhìn thấy từ bên ngoài.
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 Scope
function secret() {
var code = "1234";
}
secret();
console.log(code);[Đáp án & Giải thích]
- Bạn nghĩ:
ReferenceError: code is not defined. Vìcodeđược khai báo bên trongsecret, thuộc function scope củasecret. Từ global scope không thể nhìn thấy vào trong. - Giải thích: Function scope là "phòng kín một chiều". Từ trong nhìn ra được, từ ngoài nhìn vào không được.
P2 — Nested Function Visibility
var level = "easy";
function game() {
var level = "hard";
console.log(level);
}
game();
console.log(level);[Đáp án & Giải thích]
- Bạn nghĩ: In ra
"hard"rồi"easy". Biếnlevelbên tronggameshadow biếnlevelbên ngoài, nhưng chỉ trong scope củagame. - Giải thích: Đây là scope shadowing (sẽ học chi tiết ở Lesson 1.2.5). Mỗi scope có
levelriêng.console.log(level)ở global resolve đến globallevel.
P3 — Function Scope vs Block Scope (Preview)
function testVar() {
if (true) {
var x = 10;
}
console.log(x);
}
function testLet() {
if (true) {
let y = 20;
}
console.log(y);
}
testVar();
testLet();[Đáp án & Giải thích]
- Bạn nghĩ:
testVar()in ra10.testLet()throwReferenceError: y is not defined. - Giải thích:
varlà function-scoped: nó "nhảy" ra khỏi block{ }và thuộc về function scope.letlà block-scoped: nó bị giới hạn trong block{ }nơi nó được khai báo. Đây là lý do chínhlet/constđược khuyến nghị thayvartrong modern JavaScript.
8. Implementation Lab (Bài lab thực hành)
Lab: Private Counter (Guided → Independent)
Yêu cầu: Viết createCounter sao cho:
countchỉ tồn tại bên trong function scope.increment()vàget()có thể truy cậpcount.- Từ bên ngoài,
countkhông thể truy cập trực tiếp.
Starter (đã hoàn thành — tập trung vào việc giải thích tại sao nó hoạt động):
function createCounter() {
// count thuộc function scope của createCounter
var count = 0;
function increment() {
// TODO: Truy cập count tại đây
count++;
}
function get() {
// TODO: Truy cập count tại đây
return count;
}
return { increment, get };
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.get()); // 2
console.log(counter.count); // undefined[Đáp án tham khảo]
- Bạn nghĩ:
countkhai báo bằngvar(hoặclet/const) bên trongcreateCountersẽ thuộc function scope.incrementvàgetcùng nằm trong function scope nên nhìn thấycount. Object return chỉ chứaincrementvàget, không chứacount, nên từ bên ngoài không truy cập được. - Giải thích: Đây là nền tảng của private state bằng function scope. Kết hợp với closure (Module 1.4),
countsẽ được giữ lại qua nhiều lần gọi.
9. Edge Cases (Các trường hợp ngoại lệ)
EC-1: var trong Function Scope
function outer() {
var x = 1;
function inner() {
var x = 2;
console.log(x);
}
inner();
console.log(x);
}
outer();What fails? Nếu nghĩ var x = 2 bên trong inner ghi đè var x = 1 của outer, bạn sẽ bối rối khi outer() vẫn in ra 1.
Why? Mỗi function có scope riêng. x trong inner là một binding hoàn toàn khác x trong outer. Chúng không ghi đè lẫn nhau mà shadow (che) nhau trong scope tương ứng. var trong inner không "leak" ra outer.
EC-2: Tham số là Local Binding
function modify(value) {
value = 999;
console.log(value);
}
var value = 100;
modify(value);
console.log(value);What fails? Nếu nghĩ value = 999 bên trong modify sẽ thay đổi value bên ngoài, bạn sẽ hiểu nhầm về pass-by-value và scope.
Why? Tham số value của modify là một local binding trong function scope của modify. Dòng value = 999 chỉ thay đổi local binding, không ảnh hưởng đến value ở global scope. Tuy nhiên, nếu value là object, việc mutate property bên trong function sẽ ảnh hưởng object bên ngoài — điều này thuộc về Value Semantics (Stage 2), không phải scope.
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng): Một hàm helper trả về kết quả sai sau khi refactor. Developer nghi ngờ "biến bị ghi đè".
Reproduction (Tái hiện lỗi):
var name = "global";
function render() {
var name = "local";
helper();
}
function helper() {
console.log(name);
}
render();Evidence (Bằng chứng): Console in ra "global" thay vì "local".
Hypothesis (Giả thuyết): Developer tưởng rằng vì helper() được gọi từ bên trong render(), nên helper() sẽ nhìn thấy name của render().
Verification (Xác minh): Di chuyển helper vào bên trong render hoặc truyền name như tham số.
Root Cause (Nguyên nhân gốc rễ): Scope mismatch do nhầm lẫn dynamic scope. JavaScript dùng lexical scope, nghĩa là helper() nhìn thấy scope nơi nó được viết (global), không phải scope nơi nó được gọi (render()). name trong render() là local binding của render, không liên quan đến helper.
Fix (Sửa):
function render() {
var name = "local";
helper(name);
}
function helper(name) {
console.log(name);
}Prevention (Phòng ngừa): Khi một hàm cần dữ liệu từ scope khác, truyền qua tham số thay vì dựa vào outer scope. Điều này làm code explicit và dễ trace hơn.
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: Bạn đang review một pull request. Một junior dev viết module quản lý trạng thái form:
var formState = { dirty: false };
function validate() {
var formState = { dirty: true };
// validate logic...
}
function submit() {
console.log(formState.dirty);
}
validate();
submit();Symptom: submit() luôn log ra false dù validate() đã set dirty: true.
Decision: Developer cần nhận ra var formState bên trong validate() là một local binding hoàn toàn mới trong function scope của validate. Nó không ghi đè formState ở global scope. Đây là bug do hiểu nhầm function scope.
Fix: Không khai báo lại var formState bên trong validate nếu mục đích là mutate global state. Hoặc tốt hơn: refactor để truyền state qua tham số và return.
function validate(state) {
state.dirty = true;
return state;
}
var formState = { dirty: false };
formState = validate(formState);
submit(formState);💡 Trade-off: Truyền state qua tham số làm code explicit hơn nhưng tăng số lượng tham số. Trong production, bạn sẽ dùng module pattern hoặc state management (Stage 8) thay vì global variable.
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: "Tại sao biến khai báo bằng
varbên trong hàm không nhìn thấy từ bên ngoài? Điều gì xảy ra nếu bỏvarđi?" - Hỏi AI:
Why can't I access a var variable declared inside a function from outside? What happens if I remove var? - So sánh: AI có giải thích rõ sự khác biệt giữa local binding và implicit global không?
- Verify: Đối chiếu với MDN — Function scope.
Gợi ý
Để ý xem AI có nhầm:
- "Function scope only exists after the function is called" (sai: scope structure tồn tại tại parse time).
- "Removing
varmakes it a global variable automatically" (đúng ở non-strict, nhưng AI thường không nhắc strict mode).
Đáp án tham khảo
- Bạn nghĩ:
varbên trong hàm tạo local binding trong function scope. Từ bên ngoài, engine không thể resolve identifier đó vì nó nằm ngoài visibility zone. Nếu bỏvar, JavaScript ở non-strict mode sẽ tạo implicit global property — đây là nguồn gốc của global pollution. - AI trả lời: Thường giải thích đúng rằng
vartạo local variable, nhưng hay dùng từ mơ hồ như "the variable is trapped inside the function" hoặc "it becomes local to the function" mà không phân biệt lexical scope structure và execution context instance. - So sánh: AI hiếm khi nhấn mạnh điểm then chốt: scope được xác định bởi vị trí viết code (lexical), không phải vị trí gọi. Điều này quan trọng để hiểu closure và debug scope shadowing.
- Điểm AI nói sai hoặc quá mơ hồ: AI thường nói "variables declared inside a function are local" nhưng không giải thích tại sao — vì function tạo ra một scope boundary mà outer scope không thể nhìn qua. AI cũng thường bỏ qua strict mode khi nói về implicit global.
- Kết luận: Nếu bạn chỉ ra được function scope là lexical boundary và implicit global chỉ xảy ra ở non-strict mode, bạn đã hiểu sâu hơn AI. Điều này sẽ giúp bạn viết code an toàn hơn ở các module sau.
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 in ra
globalthay vìlocal? Tại sao không phải do biến bị ghi đè?"
var name = "global";
function render() {
var name = "local";
helper();
}
function helper() {
console.log(name);
}
render();Dùng đúng terminology: function scope, lexical scope, local binding, outer scope.
Mô phỏng
- Bạn nói:
helperđược viết ở global scope, nên lexical scope củahelperlà global. Khihelper()cần resolvename, nó nhìn vào scope nơi nó được viết — global scope — và tìm thấyname = "global". Biếnname = "local"bên trongrenderthuộc function scope củarender, hoàn toàn tách biệt. Đây không phải ghi đè mà là shadowing trong scope khác. Đểhelpernhìn thấynamecủarender, ta phải truyền qua tham số hoặc định nghĩahelperbên trongrender.
💡 Hình dung mỗi function như một căn phòng có cửa sổ một chiều: từ trong nhìn ra ngoài được, nhưng mỗi phòng chỉ nhìn thấy những gì ở bên ngoài phòng đó, không phải phòng bên cạnh.
Gợi ý đánh giá bản thân
- Đồng nghiệp có hiểu tại sao không phải do "biến bị ghi đè" không?
- Bạn có thể dự đoán được nếu đổi
varthànhletở cả hai chỗ không? - Nếu họ hỏi "vậy làm sao để helper nhìn thấy local name", bạn trả lời được ít nhất 2 cách không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Nội dung |
|---|---|---|
| Giải thích function scope là scope tạo bởi hàm | Explain (Giải thích) | Dùng lời riêng mô tả function scope và tại sao biến local không nhìn thấy từ ngoài |
| Phân biệt local và global/outer variables | Prediction (Dự đoán) | Cho code có biến cùng tên ở global và function, đoán giá trị từng vị trí |
| Dự đoán accessibility từ bên ngoài hàm | Prediction Exercise (P1–P3) | Cho 3 đoạn code, đoán biến nào accessible từ global |
Nhận diện var function-scoped vs let block-scoped | Prediction (P3) | Giải thích tại sao var thoát khỏi block nhưng không thoát khỏi function |
| Encapsulate biến bằng function scope | Implementation Lab | Viết hàm tạo private state, biến nội bộ không lộ ra ngoài |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể giải thích function scope là "phòng riêng" mà từ ngoài không nhìn vào được.
- [ ] Có thể phân biệt local binding (trong hàm) và outer binding (bên ngoài hàm).
- [ ] Dự đoán đúng 3/3 scenarios trong Prediction Exercise.
- [ ] Có thể trace được lý do
helper()nhìn thấy globalnamethay vì localnametrong Debug Lab. - [ ] Implement được
createCountervới private state bằng function scope.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Global Scope (Lesson 1.2.2) — hiểu scope ngoài cùng.
Current (Hiện tại): Function Scope — hiểu scope bên trong hàm, local vs outer, encapsulation.
Next (Tiếp theo):
- Lesson 1.2.4 — Block Scope (
{ },let,const)- Lesson 1.2.5 — Nested Scope & Shadowing
- Module 1.4 — Closure (function scope + lifetime extension)
- Stage 2 — Object Model &
this(method scope behavior)- Stage 8 — React (component function scope, hooks closure)