Lesson 1.3.3 — let and const (let và const)
0. Metadata (Thông tin bài học)
| Field | Value |
|---|---|
| Stage | 1 — JavaScript Execution Model |
| Module | 1.3 — Hoisting & Temporal Dead Zone |
| Lesson | 1.3.3 — let and const |
| Competency | C01.2 — Variables & Bindings |
| Depth | L2–L3 (Explain → Use) |
| Prerequisites | Lesson 1.3.1 (Declaration vs Initialization), Lesson 1.3.2 (var), Lesson 1.2.4 (Block Scope) |
| Cognitive Load | Medium |
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:
let count = 0;
if (true) {
let count = 10;
}
console.log(count); // 0Vớ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:
console.log(user);
let user = "Alice"; // ReferenceErrorBà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ể:
- Giải thích tại sao
letcó block scope vàvarcó function scope. - Trace một
letbinding qua Creation Phase (declare, không initialize) và Execution Phase (initialize tại dòng khai báo). - Phân biệt
letvàconstở mức initialization và re-assignment. - Dự đoán khi nào
letthrowReferenceErrorvà 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 ở đâyExecution 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
letvề scope và initialization timing. - Khác
letở hai điểm:- Bắt buộc initializer: Không thể viết
const x;— declaration và initialization phải xảy ra đồng thời. - Không cho phép re-assignment: Sau khi initialize, binding không thể được gán giá trị mới qua
=.
- Bắt buộc initializer: Không thể viết
Sai lầm phổ biến
"
letkhô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 Scope | let/const binding chỉ visible trong block { } chứa nó. |
| Deferred Initialization | let binding được declare trong Creation Phase nhưng không initialize cho đến dòng khai báo trong Execution Phase. |
| Uninitialized Binding | Trạ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.
lettạo binding trong Block Environment Record.
Awareness (Biết tồn tại)
constvới object/array: binding không đổi, nhưng nội dung object có thể mutate.let/consttrongforloop 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
CreateMutableBindingvsCreateImmutableBinding. constvớ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:
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
statustrong 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:
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:
function config() {
const BASE_URL = "https://api.example.com";
console.log(BASE_URL);
}
config();Step 1 — Creation Phase
- Declaration: Tạo binding
BASE_URLtrong 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ưngconstbắ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.
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:
2vìlet value = 2tạo binding mới trong block scope củaif. - Dòng 6:
1vì binding ngoàiifkhông bị ảnh hưởng.
- Dòng 4:
Giải thích
letcó block scope.ifblock tạo Block Environment Record riêng.let value = 2bên trongiflà binding hoàn toàn mới, shadow binding ngoài.- Khi
ifblock kết thúc, inner binding bị hủy.console.log(value)ngoàiifresolve đến outer binding →1.
Prediction 2
Đừng chạy code. Dự đoán output hoặc error.
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
letkhô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.
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
constbảo vệ binding (không cho phépuser = ...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
constvớ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:
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:
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
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 đó:
- Một biến
letđược khai báo trong mộtifblock. - Biến đó shadow một biến cùng tên ở outer scope.
- Giá trị trong block khác với giá trị ngoài block.
- 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
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
const settings = { theme: "dark" };
settings = { theme: "light" }; // TypeError: Assignment to constant variableconst 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
{
// 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.
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ưngconfiglàundefined.
Hypothesis (Giả thuyết):
letbinding được declare trong Creation Phase nhưng không được initialize cho đến dònglet config = ....console.logchạy trước initialization → TDZ error.
Verification (Xác minh): Di chuyển console.log xuống sau dòng khai báo:
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-definecó 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:
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_URLvàTIMEOUTlà giá trị không đổi trong suốt lifecycle của module. Dùngconstngăn accidental re-assignment.retryCountcần thay đổi (tăng sau mỗi lần retry). Dùngletcho phép re-assignment.- Nếu đổi
const API_URLthànhlet 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ánAPI_URL = "malicious-url"mà không có lỗi compile-time.
Giải thích
constlà 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
constmặc định và chỉ đổi sangletkhi 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:
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ộ:
// Lỗi: TypeError: Assignment to constant variable
permissions = await fetchExtraPermissions(user.id);Câu hỏi:
- Tại sao developer chọn
constchopermissionsban đầu? - 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
constcho mọi thứ là best practice" hoặc dùng auto-fix từ linter. Họ không dự đoán đượcpermissionssẽ 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ếupermissionsthực sự cần mutate. - Refactor: Tính toán
permissionsmột lần duy nhất sau khi có đủ data:jsconst extra = await fetchExtraPermissions(user.id); const permissions = isAdmin ? ["all"] : extra; - Refactor tốt hơn vì giữ
constvà tránh mutation. Nhưng nếupermissionscần update nhiều lần trong lifecycle,letlà lựa chọn đúng.
- Đổi thành
13. AI-assisted Exercise (Bài tập với AI)
Level B — Challenge (Thử thách AI)
- 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
letvàconstở 3 góc độ: scope, initialization timing, và re-assignment. - Hỏi AI: "What is the difference between
letandconstin JavaScript? Explain with examples." - 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?
- 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:
letcho phép;constcấm. - Initializer:
constbắt buộc;letkhông bắt buộc. constobject: 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ế)
- "
letallows you to reassign the variable, whileconstdoes not." - "
constis 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:
letcho re-assign,constkhông. - AI thiếu depth: Nó không giải thích initialization timing (Creation Phase vs Execution Phase). Nó thường nói "
constvalue cannot change" — mơ hồ giữa binding và value. - AI hiếm khi nhắc đến
constvới object vẫn cho phép mutation.
- AI đúng về behavior cơ bản:
Điểm AI nói sai hoặc quá mơ hồ
- AI nói "
constvalue cannot change" — câu này sai với object.constchỉ cấm re-assignment của binding, không cấm mutation. - AI không nhắc đến việc
constbắ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".
- AI nói "
Kết luận
- Nếu bạn chỉ ra được rằng
constbảo vệ binding chứ không bảo vệ value, và cảletlẫnconstđều có cùng initialization timing (deferred so vớivar), 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).
- Nếu bạn chỉ ra được rằng
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 = 5throwReferenceErrorkhi tôi cốconsole.log(x)trước dòng đó, nhưngvar x = 5thì inundefined? 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ànhundefined. Cho nên khi Execution Phase chạy đếnconsole.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ònglet x = 5, engine thấy binding tồn tại nhưng chưa được initialize → throwReferenceError." - "Dòng
let x = 5sau đó mới thực hiện initialization và assignment. Từ đó trở đi,xmới dùng được." - "Vậy nên khác biệt không phải
varđược đưa lên đầu cònletkhô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
varnhư khách được giao vé tạm (undefined) ngay khi vào danh sách.letlà 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
undefinedkhác vớiuninitializedkhông? - Bạn có thể dự đoán được behavior của
constmà không cần nhớ cú pháp không? - Nếu đồng nghiệp hỏi "vậy
letcó đượ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/const | Explain (Giải thích) + Prediction (Dự đoán) | L2 |
Trace let qua Creation/Execution Phase | Implementation (Thực hành trace) | L3 |
Phân biệt let và const ở initialization và re-assignment | Classification (Phân loại) | L2–L3 |
| Dự đoán TDZ error vs normal access | Prediction (Dự đoán) | L3 |
Debug environment mismatch khi refactor var → let | Debug Lab (Gỡ lỗi) | L3 |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể giải thích tại sao
letcó block scope thay vì function scope. - [ ] Có thể trace một
letbinding qua Creation Phase (declare, không initialize) và Execution Phase (initialize tại dòng khai báo). - [ ] Có thể phân biệt
letvàconstở mức initialization (mandatory initializer) và re-assignment. - [ ] Có thể dự đoán đúng khi nào
letthrowReferenceErrordo uninitialized binding. - [ ] Có thể giải thích tại sao
constvớ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ếtvarlà 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.constbắ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/constbindings. Để hiểu stale closure, bạn cần biết binding trong environment đã ở trạng thái nào.- Stage 2 (Object Model):
constvới object — mutation vs re-assignment sẽ được xem xét sâu hơn trong context của reference types.