Lesson 1.2.4 — Block Scope (Phạm vi Khối)
0. Metadata
| Trường | Giá trị |
|---|---|
| Stage | 1 — JavaScript Execution Model |
| Module | 1.2 — Scope & Lexical Environment |
| Lesson | 1.2.4 |
| Competency | C02 — JavaScript Runtime |
| Depth | L2–L3 (Explain → Use) |
| Prerequisites | Lesson 1.2.1 — Scope là gì?, Lesson 1.2.2 — Global Scope, Lesson 1.2.3 — Function Scope |
| Cognitive Load | Trung bình |
1. Why This Exists (Vì sao cần học)
Bạn viết một đoạn xử lý lỗi trong hàm lớn:
function processData(data) {
if (data.error) {
var errorMessage = "Invalid input";
return null;
}
console.log(errorMessage);
return data;
}Production Risk
Dòng console.log(errorMessage) không throw lỗi. Nó in ra "Invalid input" hoặc undefined — hành vi silent và dễ gây bug logic. Developer dùng var tưởng rằng biến chỉ tồn tại trong block { }, nhưng var là function-scoped và "nhảy" ra ngoài if.
Block scope trả lời câu hỏi:
"Những biến khai báo bên trong một cặp ngoặc nhọn
{ }có bị giới hạn trong đó không?"
Câu trả lời phụ thuộc vào từ khóa bạn dùng: var, let, hay const.
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 và function scope (Lesson 1.2.2, 1.2.3).
varlà function-scoped (preview từ Lesson 1.2.3).
3. Learning Objectives (Mục tiêu học tập)
Sau lesson này, bạn có thể:
- Giải thích block scope là scope được tạo ra bởi cặp ngoặc nhọn
{ }. - Phân biệt
var(function-scoped) vàlet/const(block-scoped). - Dự đoán một biến có accessible hay không từ bên ngoài block
{ }. - Nhận diện lỗi do
varleak ra khỏi block trong code production. - Refactor code từ
varsanglet/constđể tận dụng block scope.
4. Mental Model (Mô hình tư duy)
Block Scope = Ngăn Kéo Trong Phòng
Hãy hình dung function scope là một phòng riêng (Lesson 1.2.3). Bên trong phòng đó, bạn có thể đặt các ngăn kéo (block { }):
Phòng (Function Scope)
├── Ngăn kéo A (if block)
│ └── let x = 10 ← chỉ tồn tại trong ngăn kéo
├── Ngăn kéo B (for block)
│ └── const i = 0 ← chỉ tồn tại trong ngăn kéo
└── var y = 20 ← nằm trực tiếp trong phòngletvàconstđặt đồ vào ngăn kéo. Khi đóng ngăn kéo (hết block), đồ bên trong biến mất khỏi tầm nhìn.varbỏ qua ngăn kéo và đặt đồ trực tiếp lên phòng (function scope). Dù viết bên trong block, nó vẫn nhìn thấy từ bên ngoài block.
Sai lầm phổ biến
"Block
{ }luôn tạo scope."
Sai. Block { } chỉ tạo scope khi kết hợp với let hoặc const. Nếu bạn dùng var bên trong block, biến đó thuộc về function scope chứa block — block không "nhốt" được var.
5. Core Concepts (Các khái niệm cốt lõi)
| Loại | Nội dung |
|---|---|
| Essential (Bắt buộc) | Block { }, Block scope, let block-scoped, const block-scoped |
| Supporting (Hỗ trợ) | Contrast var vs let/const trong cùng một block |
| Awareness (Biết tồn tại) | TDZ (Temporal Dead Zone) — sẽ học chi tiết Module 1.3 |
| Out of Scope (Không học ở đây) | Hoisting (Module 1.3), Closure (Module 1.4), const mutation rules (Stage 2) |
6. Worked Example (Ví dụ phân tích từng bước)
Input:
function demo() {
if (true) {
var a = 1;
let b = 2;
const c = 3;
}
console.log(a);
console.log(b);
console.log(c);
}
demo();Step 1 — Observation: demo() được gọi. Bên trong if (true) { } có 3 khai báo. Sau block, 3 lần console.log cần resolve a, b, c.
Step 2 — Classification: Variable resolution với mixed scope kinds trong cùng một block.
Step 3 — Reasoning:
| Identifier | Loại khai báo | Scope thuộc về | Từ sau block nhìn thấy? | Kết quả |
|---|---|---|---|---|
a | var | Function scope (demo) | ✅ Có | 1 |
b | let | Block scope (if) | ❌ Không | ReferenceError |
c | const | Block scope (if) | ❌ Không | ReferenceError |
Step 4 — Conclusion: console.log(a) in ra 1. console.log(b) và console.log(c) đều throw ReferenceError vì b và c thuộc block scope của if, 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 Block Scope
{
const secret = "hidden";
}
console.log(secret);[Đáp án & Giải thích]
- Bạn nghĩ:
ReferenceError: secret is not defined.constkhai báo bên trong block{ }tạo block scope. Từ bên ngoài block, engine không thể resolvesecret. - Giải thích: Block scope giống function scope ở điểm "một chiều", nhưng chặt chẽ hơn: chỉ cần ra khỏi cặp ngoặc nhọn là visibility biến mất.
P2 — var Leak
function check() {
if (false) {
var x = 10;
}
console.log(x);
}
check();[Đáp án & Giải thích]
- Bạn nghĩ: In ra
undefined. Không phảiReferenceError. Vìvar xthuộc function scope củacheck, blockifkhông giới hạn đượcvar. Dùif (false)không chạy,xvẫn được hoisted (preview Module 1.3) với giá trịundefined. - Giải thích: Đây chính xác là lý do
vartrong block nguy hiểm. Developer tưởng biến "bị nhốt" trongif, nhưngvarđược xác định thuộc function scope ngay từ khi code được viết (parse time) — block{ }không thể giữ nó lại. Vì vậyvar"leak" ra toàn bộ function.
P3 — Nested Blocks
function nested() {
let a = 1;
{
let a = 2;
console.log(a);
{
let a = 3;
console.log(a);
}
}
console.log(a);
}
nested();[Đáp án & Giải thích]
- Bạn nghĩ: In ra
2,3,1. Mỗi cặp{ }tạo một block scope mới.let a = 2shadowa = 1trong block ngoài.let a = 3shadowa = 2trong block trong cùng. Dòng cuối cùng ở function scope in raa = 1. - Giải thích: Block scope có thể lồng nhau giống function scope. Mỗi block tạo một "lớp" visibility riêng.
8. Implementation Lab (Bài lab thực hành)
Lab: Secure Transaction (Guided → Independent)
Yêu cầu: Viết processPayment sao cho:
sanitizedInputchỉ tồn tại trong block validate.discountchỉ tồn tại trong block apply discount.- Không dùng
var. - Từ sau mỗi block, biến tương ứng không thể truy cập.
Starter:
function processPayment(amount, code) {
// TODO: Block validate — chỉ let/const
if (amount <= 0) {
let sanitizedInput = Math.abs(amount);
console.log("Sanitized:", sanitizedInput);
amount = sanitizedInput;
}
// TODO: Block apply discount — chỉ let/const
if (code === "SAVE10") {
const discount = 0.1;
amount = amount * (1 - discount);
}
console.log("Final:", amount);
// TODO: Thử console.log(sanitizedInput) và console.log(discount) ở đây
// Đoán kết quả trước khi chạy
}
processPayment(-100, "SAVE10");[Đáp án tham khảo]
- Bạn nghĩ:
sanitizedInputvàdiscountthuộc block scope của từngif.console.log(sanitizedInput)vàconsole.log(discount)ở cuối function sẽ throwReferenceErrorvì cả hai đều ra khỏi block scope. - Giải thích: Đây là cách dùng block scope để giới hạn lifetime visibility của biến tạm. Biến chỉ tồn tại đúng chỗ cần dùng, giảm rủi ro sử dụng nhầm ở phía dưới function.
9. Edge Cases (Các trường hợp ngoại lệ)
EC-1: for Loop Block Scope
for (let i = 0; i < 3; i++) {
const message = "round " + i;
setTimeout(() => console.log(message), 0);
}
console.log(typeof message);What fails? Nếu đổi let thành var, message sẽ nhìn thấy được sau loop và callback sẽ in ra kết quả khác (do closure + var — preview Module 1.4, chúng ta chưa học chi tiết).
Why? Mỗi lần lặp của for với let tạo một block scope riêng. message chỉ tồn tại trong block của từng iteration. typeof message ở ngoài trả về "undefined".
EC-2: Standalone Block
{
let isolated = true;
}
console.log(isolated);What fails? Một số developer tưởng rằng block { } chỉ có tác dụng khi đi kèm if/for/while.
Why? Block { } đứng một mình (standalone block statement) vẫn tạo block scope cho let/const. Đây là cách tạo scope tạm thời mà không cần function.
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng): Một hàm xử lý form có biến error bị ghi đè giữa các bước validate. Developer nghi ngờ "logic validate sai".
Reproduction (Tái hiện lỗi):
function validateForm(data) {
if (!data.email) {
var error = "Email required";
}
if (!data.password) {
var error = "Password required";
}
return error;
}
console.log(validateForm({ email: "", password: "" }));Evidence (Bằng chứng): Hàm luôn trả về "Password required" dù cả hai trường đều trống. Developer mong đợi một danh sách lỗi hoặc lỗi đầu tiên.
Hypothesis (Giả thuyết): var error trong cả hai if block thực chất là cùng một binding trong function scope. Dòng thứ hai ghi đè dòng đầu tiên.
Verification (Xác minh): Đổi var thành let — lập tức throw SyntaxError: Identifier 'error' has already been declared, vì let không cho phép redeclare trong cùng scope.
Root Cause (Nguyên nhân gốc rễ): var là function-scoped. Cả hai if block không tạo scope riêng cho var. Kết quả là error bị reassign thay vì tạo biến mới.
Fix (Sửa):
function validateForm(data) {
let errors = [];
if (!data.email) {
errors.push("Email required");
}
if (!data.password) {
errors.push("Password required");
}
return errors;
}Prevention (Phòng ngừa): Trong block logic tạm thời, luôn dùng let/const. Nếu cần accumulate kết quả qua nhiều block, khai báo accumulator ở function scope và mutate nó trong block.
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 code của một teammate trong module xử lý thanh toán:
function handlePayment(order) {
if (order.coupon) {
var rate = getDiscountRate(order.coupon);
order.total *= 1 - rate;
}
// 200 dòng code khác...
if (order.vip) {
var rate = 0.05;
order.total *= 1 - rate;
}
return order;
}Symptom: Khi cả coupon và vip đều true, discount rate cuối cùng không đúng. Hoặc nếu chỉ vip true nhưng rate từ block trên vẫn visible trong function scope và có thể bị dùng nhầm ở logic phía dưới (nếu code dài hơn).
Decision: Developer cần nhận ra var rate trong cả hai if là cùng một binding trong function scope. Không có isolation giữa hai block.
Fix: Dùng const trong từng block để đảm bảo rate chỉ tồn tại đúng nơi cần dùng. Nếu cần dùng ở nhiều block, đặt tên khác nhau hoặc refactor thành hàm riêng.
function handlePayment(order) {
if (order.coupon) {
const couponRate = getDiscountRate(order.coupon);
order.total *= 1 - couponRate;
}
// 200 dòng code khác...
if (order.vip) {
const vipRate = 0.05;
order.total *= 1 - vipRate;
}
return order;
}💡 Trade-off: Dùng
constvới tên rõ ràng (couponRate,vipRate) vừa tận dụng block scope, vừa cải thiện readability. Đây là lý do modern JavaScript khuyến nghịconstby default.
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
vartrongifblock vẫn nhìn thấy được ngoàiif, nhưngletthì không? Block{ }có tạo scope không?" - Hỏi AI:
Why is a var variable declared inside an if block accessible outside, but let is not? Does a pair of braces {} always create a scope? - So sánh: AI có nói "
varignores block scope" hay chỉ nói "letis block scoped" mà không giải thích cơ chế? - Verify: Đối chiếu với MDN — Block.
Gợi ý
Để ý xem AI có nhầm:
- "All variables inside
{}are block-scoped" (sai: chỉlet/const;varlà function-scoped). - "
varis hoisted out of the block" (mơ hồ:varkhông bị "hoisted ra khỏi block" mà thuộc function scope ngay từ đầu).
Đáp án tham khảo
- Bạn nghĩ: Block
{ }tạo scope chỉ khi kết hợp vớilet/const.varkhông tôn trọng block scope — nó thuộc về function scope chứa block. Vì vậyvartrongifvẫn nhìn thấy được ngoàiif(trong cùng function), cònlet/constthì bị giới hạn trong block. - AI trả lời: Thường trả lời đúng rằng
letblock-scoped vàvarfunction-scoped, nhưng hay dùng từ mơ hồ như "varleaks out of the block" hoặc "vardoesn't care about blocks". - So sánh: AI thường không phân biệt rõ: block statement
{ }luôn tạo ra một lexical block, nhưngvarkhông bind vào block đó. Điểm mù là AI diễn đạt như thể block chỉ "bắt"let/constchứ không phải block tạo scope vàvarchọn không dùng nó. - Điểm AI nói sai hoặc quá mơ hồ: AI hay nói "
varis hoisted to the top of the function" — điều này đúng nhưng gây hiểu nhầm rằngvar"di chuyển" vị trí. Thực tế scope củavarđược xác định tại parse time là function scope, không phải nó "nhảy" ra khỏi block khi chạy. - Kết luận: Nếu bạn chỉ ra được block scope là cấu trúc tĩnh do parser tạo ra, và
var/let/constlà những cách khác nhau để bind vào scope, bạn đã hiểu sâu hơn AI. Điều này sẽ giúp bạn debug hoisting và TDZ ở Module 1.3.
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 không lỗi ở dòng
console.log(a)nhưng lỗi ởconsole.log(b)?"
function demo() {
if (true) {
var a = 1;
let b = 2;
}
console.log(a);
console.log(b);
}Dùng đúng terminology: block scope, function scope, block-scoped, function-scoped.
Mô phỏng
- Bạn nói:
var a = 1thuộc function scope củademo. Dù viết bên trongif,varbỏ qua block scope và bind trực tiếp vào function. Vì vậy sau khi ra khỏiif,avẫn nhìn thấy được. Ngược lại,let b = 2thuộc block scope củaif. Block scope chỉ tồn tại trong cặp ngoặc nhọn{ }. Khi ra khỏiif,bbiến mất khỏi visibility zone, nênconsole.log(b)throwReferenceError. Đây là lý do chúng ta dùnglet/constthay vìvar— để biến chỉ tồn tại đúng chỗ cần dùng.
💡 Hình dung
if { }như một chiếc hộp trong suốt:letđể đồ vào hộp thì ra ngoài không thấy;varxuyên qua đáy hộp rơi xuống sàn phòng.
Gợi ý đánh giá bản thân
- Đồng nghiệp có hiểu tại sao
var"rơi xuống sàn phòng" không? - Bạn có thể dự đoán được hành vi của
consttrong ví dụ tương tự không? - Nếu họ hỏi "vậy
vartrongforloop thì sao", 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 |
|---|---|---|
Giải thích block scope là scope tạo bởi { } | Explain (Giải thích) | Dùng lời riêng mô tả block scope và tại sao nó chặt chẽ hơn function scope |
Phân biệt var (function-scoped) và let/const (block-scoped) | Prediction (Dự đoán) | Cho code có cả var và let trong cùng block, đoán biến nào accessible sau block |
Dự đoán accessibility sau block { } | Prediction Exercise (P1–P3) | Cho 3 đoạn code với block lồng nhau, đoán output hoặc lỗi |
Nhận diện var leak khỏi block | Debug Lab | Tìm root cause biến bị ghi đè giữa các block do dùng var |
Refactor từ var sang let/const | Implementation Lab | Viết lại code sử dụng block scope để isolate biến tạm |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể giải thích block scope là "ngăn kéo" bên trong function, chỉ tồn tại trong
{ }. - [ ] Có thể phân biệt
var(function-scoped) vàlet/const(block-scoped). - [ ] Dự đoán đúng 3/3 scenarios trong Prediction Exercise.
- [ ] Có thể trace được lý do
varbị ghi đè giữa các block trong Debug Lab. - [ ] Refactor được code để isolate biến tạm bằng
let/consttrong block.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Function Scope (Lesson 1.2.3) — hiểu scope bên trong hàm.
Current (Hiện tại): Block Scope — hiểu scope bên trong
{ },let/const, và sự khác biệt vớivar.Next (Tiếp theo):
- Lesson 1.2.5 — Nested Scope & Shadowing
- Lesson 1.2.6 — Variable Resolution
- Module 1.3 — Hoisting & TDZ (tại sao
varleak vàletthrow error trước khai báo)- Module 1.4 — Closure (block scope trong loop + callback)
- Stage 8 — React (hooks closure, stale closure với block-scoped variables)