Skip to content

Lesson 1.3.3 — let and const (let và const) ​

Bài 1.3.3 — let và const
Block Scope, Lexical Bindings & Initialization Rules • 26 phút
0:00 / 0:00

0. Metadata (Thông tin bài học) ​

FieldValue
Stage1 — JavaScript Execution Model
Module1.3 — Hoisting & Temporal Dead Zone
Lesson1.3.3 — let and const
CompetencyC01.2 — Variables & Bindings
DepthL2–L3 (Explain → Use)
PrerequisitesLesson 1.3.1 (Declaration vs Initialization), Lesson 1.3.2 (var), Lesson 1.2.4 (Block Scope)
Cognitive LoadMedium

1. Why This Exists (Vì sao cần học) ​

Bạn vừa học xong var — function scope, early initialize, cho phép re-declare. Giờ bạn mở một codebase hiện đại và thấy toàn let và const. Tại sao JavaScript cần thêm hai từ khóa này? Chúng chỉ là "var tốt hơn" hay có cơ chế hoàn toàn khác?

Hãy xem đoạn code sau:

js
let count = 0;
if (true) {
  let count = 10;
}
console.log(count); // 0

Với var, dòng cuối sẽ in 10. Với let, nó in 0. Sự khác biệt không nằm ở "syntax mới" — nó nằm ở cách engine tạo và initialize binding.

Nếu bạn nghĩ let chỉ là var với block scope, bạn sẽ không hiểu tại sao đoạn này lại fail:

js
console.log(user);
let user = "Alice"; // ReferenceError

Bài này xây dựng mental model chính xác về let và const như block-scoped bindings với lazy initialization — và tại sao điều đó tạo ra Temporal Dead Zone.

2. Prerequisites (Yêu cầu đầu vào) ​

Trước khi học bài này, bạn cần:

  • Phân biệt được Declaration, Initialization, Assignment (Lesson 1.3.1).
  • Hiểu var được declare + initialize (undefined) trong Creation Phase (Lesson 1.3.2).
  • Biết Block Scope là gì (Lesson 1.2.4).

3. Learning Objectives (Mục tiêu học tập) ​

Sau bài này, bạn có thể:

  1. Giải thích tại sao let có block scope và var có function scope.
  2. Trace một let binding qua Creation Phase (declare, không initialize) và Execution Phase (initialize tại dòng khai báo).
  3. Phân biệt let và const ở mức initialization và re-assignment.
  4. Dự đoán khi nào let throw ReferenceError và khi nào nó hoạt động bình thường.

4. Mental Model (Mô hình tư duy) ​

Mental Model: let là Block-Scoped Binding với Deferred Initialization

let x = 10;

Creation Phase (trong block/function):

1. Declare: Tạo binding 'x' trong Block Environment Record
2. Initialize: KHÔNG xảy ra ở đây

Execution Phase (tại dòng code):

3. Initialize + Assignment: Gán giá trị đầu tiên cho binding

Đặc điểm của let:

  • Block scope: Binding chỉ tồn tại trong block { } chứa nó.
  • Deferred initialize: Binding được declare trong Creation Phase nhưng ở trạng thái uninitialized cho đến dòng khai báo.
  • No re-declare: Không cho phép khai báo trùng tên trong cùng scope.

Mental Model: const là let với Immutable Binding

const API_URL = "https://api.example.com";
  • Giống let về scope và initialization timing.
  • Khác let ở hai điểm:
    1. Bắt buộc initializer: Không thể viết const x; — declaration và initialization phải xảy ra đồng thời.
    2. Không cho phép re-assignment: Sau khi initialize, binding không thể được gán giá trị mới qua =.

Sai lầm phổ biến

"let không được hoisted."

Sai. let được đưa vào scope trong Creation Phase (đó chính là declaration). Điều khác biệt là nó không được initialize trong Creation Phase. Binding đã tồn tại — bạn chỉ không được phép chạm vào nó cho đến khi initialize.

5. Core Concepts (Các khái niệm cốt lõi) ​

Essential (Bắt buộc) ​

ConceptĐịnh nghĩa
Block Scopelet/const binding chỉ visible trong block { } chứa nó.
Deferred Initializationlet binding được declare trong Creation Phase nhưng không initialize cho đến dòng khai báo trong Execution Phase.
Uninitialized BindingTrạng thái của let binding trước dòng khai báo. Truy cập sẽ throw ReferenceError.
Mandatory Initializer (const)const bắt buộc phải có giá trị khởi tạo trong cùng câu khai báo.
Immutable Binding (const)const không cho phép re-assignment sau initialization.

Supporting (Hỗ trợ) ​

  • TDZ (Temporal Dead Zone): Khoảng từ đầu block đến dòng khai báo let/const. Sẽ học sâu ở 1.3.4.
  • Environment Record: Nơi lưu trữ bindings. let tạo binding trong Block Environment Record.

Awareness (Biết tồn tại) ​

  • const với object/array: binding không đổi, nhưng nội dung object có thể mutate.
  • let/const trong for loop tạo binding mới mỗi iteration.

Out of Scope (Không học trong bài này) ​

  • Chi tiết exact algorithm của CreateMutableBinding vs CreateImmutableBinding.
  • const với primitive vs reference type sâu (sẽ học ở S2 Object Model).

6. Worked Example (Ví dụ phân tích từng bước) ​

Trace đoạn code sau:

js
function setup() {
  console.log(status);
  let status = "active";
  console.log(status);
}
setup();

Step 1 — Creation Phase (Function Execution Context của setup)

  • Engine quét function body, thấy let status.
  • Declaration: Tạo binding status trong Block Environment Record của function body.
  • Initialization: Không xảy ra. Binding ở trạng thái uninitialized.

Trạng thái Environment Record:

text
status: <uninitialized>  (declared but NOT initialized)

Step 2 — Execution Phase

  • Dòng 2: console.log(status) — engine cố gắng đọc binding chưa initialize.
  • Kết quả: ReferenceError: Cannot access 'status' before initialization.
  • Dòng 3: let status = "active" — binding được initialize với giá trị "active".
  • Dòng 4: console.log(status) — đọc binding → "active".

Code Review Lens

Khi thấy let x = ..., hãy tách thành:

  • let x → Creation Phase (D, không I)
  • x = ... → Execution Phase (I + A)

Bây giờ so sánh với const:

js
function config() {
  const BASE_URL = "https://api.example.com";
  console.log(BASE_URL);
}
config();

Step 1 — Creation Phase

  • Declaration: Tạo binding BASE_URL trong Block Environment Record.
  • Initialization: Không xảy ra trong Creation Phase.

Step 2 — Execution Phase

  • Dòng 2: const BASE_URL = "..." — Initialization + Assignment xảy ra tại dòng này. (Declaration đã xảy ra trong Creation Phase, nhưng const bắt buộc phải initialize ngay tại dòng khai báo.)
  • Dòng 3: console.log(BASE_URL) → "https://api.example.com".

WARNING

const BASE_URL; mà không có initializer sẽ throw SyntaxError ngay tại parse time — trước cả khi Creation Phase chạy.

7. Prediction Exercise (Bài tập dự đoán) ​

Prediction 1

Đừng chạy code. Dự đoán output hoặc error, và giải thích bằng Creation Phase / Execution Phase.

js
function demo() {
  let value = 1;
  if (true) {
    let value = 2;
    console.log(value);
  }
  console.log(value);
}
demo();
[Đáp án & Giải thích]
  • Bạn nghĩ

    • Dòng 4: 2 vì let value = 2 tạo binding mới trong block scope của if.
    • Dòng 6: 1 vì binding ngoài if không bị ảnh hưởng.
  • Giải thích

    • let có block scope. if block tạo Block Environment Record riêng.
    • let value = 2 bên trong if là binding hoàn toàn mới, shadow binding ngoài.
    • Khi if block kết thúc, inner binding bị hủy. console.log(value) ngoài if resolve đến outer binding → 1.

Prediction 2

Đừng chạy code. Dự đoán output hoặc error.

js
let a = 1;
let a = 2;
console.log(a);
[Đáp án & Giải thích]
  • Bạn nghĩ

    • SyntaxError: Identifier 'a' has already been declared.
  • Giải thích

    • let không cho phép re-declare trong cùng scope.
    • Engine phát hiện trùng tên binding trong cùng Lexical Environment và throw ngay tại parse/creation time.
    • Khác với var — var a = 1; var a = 2; là hợp lệ.

Prediction 3 — Transfer Exercise

Đừng chạy code. Dự đoán output hoặc error.

js
const user = { name: "Alice" };
user.name = "Bob";
console.log(user.name);
[Đáp án & Giải thích]
  • Bạn nghĩ

    • "Bob". Không có error.
  • Giải thích

    • const bảo vệ binding (không cho phép user = ... mới), không bảo vệ giá trị bên trong object.
    • user.name = "Bob" là mutation của object, không phải re-assignment của binding.
    • Đây là behavior quan trọng cần nhớ khi dùng const với object/array.

8. Implementation Lab (Bài lab thực hành) ​

Level 1 — Guided (Hướng dẫn) ​

Viết comment mô tả phase cho đoạn code sau:

js
function init() {
  // Creation Phase: let config → declare trong Block Environment Record, chưa initialize
  console.log(config);
  // Execution Phase: initialize + assignment tại dòng khai báo
  let config = { env: "prod" };
  console.log(config);
}

Level 2 — Partial Scaffold (Khung mẫu một phần) ​

Điền var hoặc let/const để đạt được behavior mong muốn:

js
function example() {
  // Goal: x có block scope, không thể truy cập ngoài if
  if (true) {
    ___ x = 10;
  }
  console.log(x); // Goal: ReferenceError
}
Đáp án
js
function example() {
  if (true) {
    let x = 10; // hoặc const x = 10;
  }
  console.log(x); // ReferenceError: x is not defined
}

Level 3 — Independent (Tự thực hiện) ​

Viết một function ngắn (tối đa 8 dòng) trong đó:

  1. Một biến let được khai báo trong một if block.
  2. Biến đó shadow một biến cùng tên ở outer scope.
  3. Giá trị trong block khác với giá trị ngoài block.
  4. Giải thích bằng đúng 3 khái niệm: block scope, declaration, shadowing.

9. Edge Cases (Các trường hợp ngoại lệ) ​

Edge Case 1: let mà không có initializer ​

js
let count;
console.log(count); // undefined
count = 5;

Dòng let count; trong Execution Phase thực hiện initialization với undefined. (Binding đã được declare trong Creation Phase, nhưng chưa initialize.) Binding không bao giờ ở trạng thái uninitialized khi dòng này đã chạy xong. Điểm then chốt: let mà không có initializer vẫn initialize thành undefined tại dòng khai báo — khác biệt so với let với giá trị explicit chỉ ở giá trị, không ở trạng thái uninitialized.

Edge Case 2: const với object mutation ​

js
const settings = { theme: "dark" };
settings = { theme: "light" }; // TypeError: Assignment to constant variable
js
const settings = { theme: "dark" };
settings.theme = "light"; // Hợp lệ
console.log(settings.theme); // "light"

TIP

Trong production, const với mutable object là common source of confusion. Nếu bạn cần immutable object, phải dùng Object.freeze() hoặc structural sharing pattern (sẽ học ở Stage 2 và Stage 8).

Edge Case 3: TDZ bắt đầu từ đầu block ​

js
{
  // TDZ của `value` bắt đầu ở đây, ngay sau dòng mở block `{`
  console.log(value); // ReferenceError
  let value = 5;
}

Temporal Dead Zone không bắt đầu tại dòng let value = 5; nó bắt đầu từ đầu lexical scope chứa nó (ngay sau dòng mở block {). Điều này chứng minh declaration đã xảy ra trong Creation Phase, trước khi bất kỳ dòng code nào trong Execution Phase chạy.

10. Debug Lab (Bài lab gỡ lỗi) ​

Symptom (Triệu chứng): Developer refactor từ var sang let và app crash với ReferenceError.

js
function initApp() {
  console.log(config);
  let config = loadConfig();
  return config;
}

Reproduction (Tái hiện lỗi): Chạy initApp() → ReferenceError: Cannot access 'config' before initialization.

Evidence (Bằng chứng):

  • Stack trace chỉ dòng console.log(config).
  • Nếu đổi lại var, code chạy nhưng config là undefined.

Hypothesis (Giả thuyết):

  • let binding được declare trong Creation Phase nhưng không được initialize cho đến dòng let config = ....
  • console.log chạy trước initialization → TDZ error.

Verification (Xác minh): Di chuyển console.log xuống sau dòng khai báo:

js
function initApp() {
  let config = loadConfig();
  console.log(config);
  return config;
}

Code chạy. Giả thuyết đúng.

Root Cause (Nguyên nhân gốc rễ): Developer nhầm lẫn giữa "không được hoisted" (sai) và "được declare nhưng chưa initialize" (đúng). let có được đưa vào Environment Record trong Creation Phase, nhưng ở trạng thái uninitialized.

Fix (Sửa): Không đọc biến trước dòng khai báo khi dùng let/const.

Prevention (Phòng ngừa):

  • Khi refactor var → let, luôn kiểm tra xem có đoạn code nào đọc biến trước declaration không.
  • ESLint rule no-use-before-define có thể bắt lỗi này tự động.

11. Design Exercise (Bài tập thiết kế giải pháp) ​

Depth L2–L3: Decision (Quyết định)

Bạn đang code một module cấu hình:

js
const API_URL = "https://api.example.com";
const TIMEOUT = 5000;
let retryCount = 0;

Câu hỏi: Tại sao API_URL và TIMEOUT dùng const, còn retryCount dùng let? Điều gì sẽ xảy ra nếu đổi const API_URL thành let API_URL?

[Đáp án tham khảo]
  • Bạn nghĩ

    • API_URL và TIMEOUT là giá trị không đổi trong suốt lifecycle của module. Dùng const ngăn accidental re-assignment.
    • retryCount cần thay đổi (tăng sau mỗi lần retry). Dùng let cho phép re-assignment.
    • Nếu đổi const API_URL thành let API_URL, code vẫn chạy nhưng mất bảo vệ binding. Một developer khác có thể vô tình gán API_URL = "malicious-url" mà không có lỗi compile-time.
  • Giải thích

    • const là tín hiệu intent: "binding này không được thay đổi". Nó không chỉ là optimization — nó là documentation.
    • Trong production, dùng const mặc định và chỉ đổi sang let khi bạn biết rõ binding cần thay đổi. Đây là convention phổ biến trong modern JavaScript.

12. Production Scenario (Tình huống thực tế) ​

Bạn review một PR trong đó junior developer dùng const cho tất cả biến:

js
const user = fetchUser();
const isAdmin = user.role === "admin";
const permissions = isAdmin ? ["all"] : ["read"];

Sau đó họ cần cập nhật permissions dựa trên một API call bất đồng bộ:

js
// Lỗi: TypeError: Assignment to constant variable
permissions = await fetchExtraPermissions(user.id);

Câu hỏi:

  1. Tại sao developer chọn const cho permissions ban đầu?
  2. Cách fix nào là tốt nhất: đổi thành let, hay refactor logic?
[Đáp án tham khảo]
  • Câu 1: Developer có thể nghĩ "dùng const cho mọi thứ là best practice" hoặc dùng auto-fix từ linter. Họ không dự đoán được permissions sẽ cần thay đổi.

  • Câu 2: Cả hai cách đều hợp lệ:

    • Đổi thành let: Đơn giản, rõ ràng nếu permissions thực sự cần mutate.
    • Refactor: Tính toán permissions một lần duy nhất sau khi có đủ data:
      js
      const extra = await fetchExtraPermissions(user.id);
      const permissions = isAdmin ? ["all"] : extra;
    • Refactor tốt hơn vì giữ const và tránh mutation. Nhưng nếu permissions cần update nhiều lần trong lifecycle, let là lựa chọn đúng.

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

Level B — Challenge (Thử thách AI)

  1. Tự trả lời trước: Viết ra giấy định nghĩa của bạn về sự khác biệt giữa let và const ở 3 góc độ: scope, initialization timing, và re-assignment.
  2. Hỏi AI: "What is the difference between let and const in JavaScript? Explain with examples."
  3. So sánh: AI có nhắc đến Creation Phase không? Nó có phân biệt rõ "binding immutable" vs "value immutable" không?
  4. Verify: Kiểm tra lại bằng MDN hoặc ECMAScript specification (từ khóa: Let and Const Declarations, CreateImmutableBinding).

Gợi ý

Để ý xem AI có nói "const makes the value immutable" không. Nếu có, đó là misconception phổ biến mà bạn đã vượt qua. const bảo vệ binding, không bảo vệ giá trị bên trong object.

Đáp án tham khảo
  • Bạn nghĩ

    • Scope: Cả hai đều block scope.
    • Initialization: Cả hai đều declare trong Creation Phase, initialize tại dòng khai báo trong Execution Phase.
    • Re-assignment: let cho phép; const cấm.
    • Initializer: const bắt buộc; let không bắt buộc.
    • const object: binding không đổi, nhưng object content có thể mutate.
  • AI trả lời (mô phỏng phản hồi thực tế)

    • "let allows you to reassign the variable, while const does not."
    • "const is used for constants that should not change."
    • "Both have block scope, unlike var."
    • "With const, you can't change the value after declaration."
  • So sánh

    • AI đúng về behavior cơ bản: let cho re-assign, const không.
    • AI thiếu depth: Nó không giải thích initialization timing (Creation Phase vs Execution Phase). Nó thường nói "const value cannot change" — mơ hồ giữa binding và value.
    • AI hiếm khi nhắc đến const với object vẫn cho phép mutation.
  • Điểm AI nói sai hoặc quá mơ hồ

    • AI nói "const value cannot change" — câu này sai với object. const chỉ cấm re-assignment của binding, không cấm mutation.
    • AI không nhắc đến việc const bắt buộc initializer trong cùng dòng.
    • AI thường không phân biệt "binding được tạo trong Creation Phase" vs "initialize tại dòng khai báo".
  • Kết luận

    • Nếu bạn chỉ ra được rằng const bảo vệ binding chứ không bảo vệ value, và cả let lẫn const đều có cùng initialization timing (deferred so với var), bạn đã hiểu sâu hơn AI. Concept này sẽ quay lại ở bài 1.3.4 (TDZ) và Stage 2 (Object mutation).

14. Teach Back (Dạy lại) ​

Yêu cầu: Giải thích cho một đồng nghiệp junior trong 2 phút:

"Tại sao let x = 5 throw ReferenceError khi tôi cố console.log(x) trước dòng đó, nhưng var x = 5 thì in undefined? Dùng đúng terminology: Creation Phase, Execution Phase, initialize. Không được dùng từ 'hoisted'."

Mô phỏng
  • Bạn nói
    • "Khi JavaScript chạy một function, engine làm hai việc: Creation Phase trước, Execution Phase sau."
    • "Trong Creation Phase, engine nhìn qua code và tạo tất cả bindings trong Environment Record."
    • "Với var x = 5, engine không chỉ tạo binding mà còn initialize nó ngay lập tức thành undefined. Cho nên khi Execution Phase chạy đến console.log(x) trước dòng khai báo, x đã có giá trị hợp lệ — dù là undefined."
    • "Với let x = 5, engine vẫn declare binding trong Creation Phase, nhưng nó KHÔNG initialize. Binding tồn tại nhưng ở trạng thái 'chưa sẵn sàng'."
    • "Khi console.log(x) chạy trước dòng let x = 5, engine thấy binding tồn tại nhưng chưa được initialize → throw ReferenceError."
    • "Dòng let x = 5 sau đó mới thực hiện initialization và assignment. Từ đó trở đi, x mới dùng được."
    • "Vậy nên khác biệt không phải var được đưa lên đầu còn let không. Cả hai đều được đưa vào scope trước. Khác biệt là initialization xảy ra khi nào."

💡 Liên tưởng: Tưởng tượng var như khách được giao vé tạm (undefined) ngay khi vào danh sách. let là khách có tên trong danh sách nhưng chưa có vé — nếu cố vào hội trường trước khi nhận vé, bị bảo vệ đuổi ra (ReferenceError).

Lưu ý: Đây là analogy để hình dung initialization timing. Ở level cao hơn (S11+), bạn cần reasoning trực tiếp qua Environment Record thay vì analogy.

Gợi ý đánh giá bản thân

  • Đồng nghiệp có hiểu tại sao undefined khác với uninitialized không?
  • Bạn có thể dự đoán được behavior của const mà không cần nhớ cú pháp không?
  • Nếu đồng nghiệp hỏi "vậy let có được hoisted không?", bạn trả lời được không mà không dùng từ đó?

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giáDepth
Giải thích block scope của let/constExplain (Giải thích) + Prediction (Dự đoán)L2
Trace let qua Creation/Execution PhaseImplementation (Thực hành trace)L3
Phân biệt let và const ở initialization và re-assignmentClassification (Phân loại)L2–L3
Dự đoán TDZ error vs normal accessPrediction (Dự đoán)L3
Debug environment mismatch khi refactor var → letDebug Lab (Gỡ lỗi)L3

16. Exit Criteria (Tiêu chí qua bài) ​

  • [ ] Có thể giải thích tại sao let có block scope thay vì function scope.
  • [ ] Có thể trace một let binding qua Creation Phase (declare, không initialize) và Execution Phase (initialize tại dòng khai báo).
  • [ ] Có thể phân biệt let và const ở mức initialization (mandatory initializer) và re-assignment.
  • [ ] Có thể dự đoán đúng khi nào let throw ReferenceError do uninitialized binding.
  • [ ] Có thể giải thích tại sao const với object vẫn cho phép mutation.
  • [ ] Có thể dự đoán đúng 3/3 scenarios trong Prediction Exercise mà không chạy code.

17. Spiral Connection (Liên kết xoắn ốc) ​

Previous (Trước): var → bạn đã biết var là function-scoped binding được declare + initialize (undefined) trong Creation Phase, cho phép early access và re-declaration.

Current (Hiện tại): let/const → bạn hiểu rằng cả hai đều là block-scoped bindings được declare trong Creation Phase nhưng không initialize cho đến dòng khai báo. const bắt buộc initializer và cấm re-assignment.

Next (Tiếp theo):

  • 1.3.4 (TDZ): Hiểu chính xác Temporal Dead Zone — khoảng thời gian từ đầu block đến dòng khai báo let/const, trong đó binding đã tồn tại nhưng chưa initialize.
  • 1.4 (Closure): Closure giữ reference đến Environment Record chứa let/const bindings. Để hiểu stale closure, bạn cần biết binding trong environment đã ở trạng thái nào.
  • Stage 2 (Object Model): const với object — mutation vs re-assignment sẽ được xem xét sâu hơn trong context của reference types.
📴 Offline Mode — Content served from cache