Skip to content

Lesson 1.2.4 — Block Scope (Phạm vi Khối) ​

Bài 1.2.4 — Block Scope
let, const, Blocks & Lexical Bindings • 24 phút
0:00 / 0:00

0. Metadata ​

TrườngGiá trị
Stage1 — JavaScript Execution Model
Module1.2 — Scope & Lexical Environment
Lesson1.2.4
CompetencyC02 — JavaScript Runtime
DepthL2–L3 (Explain → Use)
PrerequisitesLesson 1.2.1 — Scope là gì?, Lesson 1.2.2 — Global Scope, Lesson 1.2.3 — Function Scope
Cognitive LoadTrung 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:

js
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).
  • var là 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ể:

  1. Giải thích block scope là scope được tạo ra bởi cặp ngoặc nhọn { }.
  2. Phân biệt var (function-scoped) và let/const (block-scoped).
  3. Dự đoán một biến có accessible hay không từ bên ngoài block { }.
  4. Nhận diện lỗi do var leak ra khỏi block trong code production.
  5. Refactor code từ var sang let/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 { }):

text
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òng
  • let và 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.
  • var bỏ 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ạiNộ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:

js
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:

IdentifierLoại khai báoScope thuộc vềTừ sau block nhìn thấy?Kết quả
avarFunction scope (demo)✅ Có1
bletBlock scope (if)❌ KhôngReferenceError
cconstBlock scope (if)❌ KhôngReferenceError

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 ​

js
{
  const secret = "hidden";
}
console.log(secret);
[Đáp án & Giải thích]
  • Bạn nghĩ: ReferenceError: secret is not defined. const khai báo bên trong block { } tạo block scope. Từ bên ngoài block, engine không thể resolve secret.
  • 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 ​

js
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ải ReferenceError. Vì var x thuộc function scope của check, block if không giới hạn được var. Dù if (false) không chạy, x vẫn được hoisted (preview Module 1.3) với giá trị undefined.
  • Giải thích: Đây chính xác là lý do var trong block nguy hiểm. Developer tưởng biến "bị nhốt" trong if, nhưng var đượ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ậy var "leak" ra toàn bộ function.

P3 — Nested Blocks ​

js
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 = 2 shadow a = 1 trong block ngoài. let a = 3 shadow a = 2 trong block trong cùng. Dòng cuối cùng ở function scope in ra a = 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:

  • sanitizedInput chỉ tồn tại trong block validate.
  • discount chỉ 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:

js
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ĩ: sanitizedInput và discount thuộc block scope của từng if. console.log(sanitizedInput) và console.log(discount) ở cuối function sẽ throw ReferenceError vì 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 ​

js
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 ​

js
{
  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):

js
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):

js
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:

js
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.

js
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 const vớ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ị const by default.

13. AI-Assisted Exercise (Bài tập với AI) ​

Level A — Ask

  1. Tự trả lời trước: Viết ra giấy: "Tại sao var trong if block vẫn nhìn thấy được ngoài if, nhưng let thì không? Block { } có tạo scope không?"
  2. 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?
  3. So sánh: AI có nói "var ignores block scope" hay chỉ nói "let is block scoped" mà không giải thích cơ chế?
  4. 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; var là function-scoped).
  • "var is hoisted out of the block" (mơ hồ: var khô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ới let/const. var không tôn trọng block scope — nó thuộc về function scope chứa block. Vì vậy var trong if vẫn nhìn thấy được ngoài if (trong cùng function), còn let/const thì bị giới hạn trong block.
  • AI trả lời: Thường trả lời đúng rằng let block-scoped và var function-scoped, nhưng hay dùng từ mơ hồ như "var leaks out of the block" hoặc "var doesn'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ưng var không bind vào block đó. Điểm mù là AI diễn đạt như thể block chỉ "bắt" let/const chứ không phải block tạo scope và var chọn không dùng nó.
  • Điểm AI nói sai hoặc quá mơ hồ: AI hay nói "var is hoisted to the top of the function" — điều này đúng nhưng gây hiểu nhầm rằng var "di chuyển" vị trí. Thực tế scope của var đượ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/const là 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)?"

js
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 = 1 thuộc function scope của demo. Dù viết bên trong if, var bỏ qua block scope và bind trực tiếp vào function. Vì vậy sau khi ra khỏi if, a vẫn nhìn thấy được. Ngược lại, let b = 2 thuộc block scope của if. Block scope chỉ tồn tại trong cặp ngoặc nhọn { }. Khi ra khỏi if, b biến mất khỏi visibility zone, nên console.log(b) throw ReferenceError. Đây là lý do chúng ta dùng let/const thay 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; var xuyê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 const trong ví dụ tương tự không?
  • Nếu họ hỏi "vậy var trong for loop 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 blockDebug LabTìm root cause biến bị ghi đè giữa các block do dùng var
Refactor từ var sang let/constImplementation LabViế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 var bị ghi đè giữa các block trong Debug Lab.
  • [ ] Refactor được code để isolate biến tạm bằng let/const trong 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ới var.

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 var leak và let throw 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)
📴 Offline Mode — Content served from cache