Lesson 1.3.1 — Declaration vs Initialization (Khai báo vs Khởi tạo)
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.1 — Declaration vs Initialization |
| Competency | C01.2 — Variables & Bindings |
| Depth | L2–L3 (Explain → Use) |
| Prerequisites | Execution Context (1.1), Scope & Lexical Environment (1.2), familiarity with let/const/var syntax |
| Cognitive Load | Medium |
1. Why This Exists (Vì sao cần học)
Bạn refactor một đoạn code từ var sang let và bất ngờ gặp lỗi:
console.log(user); // undefined (với var)
// ...
let user = "Alice"; // ReferenceError nếu đổi sang letNhiều developer giải thích hiện tượng này bằng câu:
"Vì
varđược hoisted cònletthì không."
Nhưng câu trả lời này che giấu một cơ chế quan trọng hơn: sự khác biệt giữa declaration (khai báo), initialization (khởi tạo), và assignment (gán giá trị). Nếu không phân biệt được ba phase này, bạn sẽ không bao giờ hiểu tại sao Temporal Dead Zone tồn tại, tại sao closure giữ reference theo cách nó giữ, và tại sao đoạn code dưới đây lại fail:
const config;
config = { api: "https://api.example.com" }; // SyntaxErrorBài học này xây dựng mental model về vòng đời của một binding trước khi chúng ta bàn đến hoisting và TDZ ở các bài sau.
2. Prerequisites (Yêu cầu đầu vào)
Trước khi học bài này, bạn cần:
- Hiểu Execution Context có Creation Phase và Execution Phase (Lesson 1.1.4).
- Hiểu Lexical Environment chứa Environment Record với các bindings (Lesson 1.2.7).
- Biết cú pháp
let,const,varở mức sử dụng (Stage 0).
Nếu thiếu, quay lại Module 1.1 và 1.2 trước.
3. Learning Objectives (Mục tiêu học tập)
Sau bài này, bạn có thể:
- Phân biệt declaration, initialization, và assignment trong một đoạn code JavaScript.
- Giải thích tại sao
varcho phép truy cập trước dòng khai báo (với giá trịundefined) trong khilet/constlại throw error. - Trace trạng thái của Environment Record qua từng phase của vòng đời biến.
- Dự đoán output hoặc error của code dựa trên việc binding đã được declare, initialize, hay assign chưa.
4. Mental Model (Mô hình tư duy)
Mental Model: Variable Lifecycle (Vòng đời biến)
Một binding trong JavaScript không xuất hiện trong một bước duy nhất. Nó trải qua ít nhất 3 phase:
Declaration (Khai báo)
↓
Initialization (Khởi tạo)
↓
Assignment (Gán giá trị)- Declaration: Engine tạo tên binding trong Environment Record. Binding này "tồn tại" nhưng chưa có giá trị hợp lệ.
- Initialization: Binding được gắn một giá trị ban đầu hợp lệ (có thể là
undefinedhoặc giá trị explicit). - Assignment: Giá trị hiện tại của binding được thay thế bằng giá trị mới.
Sai lầm phổ biến
"Khai báo = khởi tạo = gán."
Không. let x = 10 trông như một dòng, nhưng engine xử lý nó qua nhiều phase khác nhau tùy loại khai báo. Sự khác biệt này là nền tảng của hoisting và TDZ.
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
| Concept | Định nghĩa |
|---|---|
| Declaration | Tạo identifier binding trong Environment Record của lexical scope. |
| Initialization | Gắn giá trị ban đầu hợp lệ vào binding (có thể là undefined). |
| Assignment | Thay thế giá trị hiện tại của binding bằng giá trị mới. |
| Uninitialized binding | Binding đã được declare nhưng chưa được initialize. Truy cập sẽ throw ReferenceError. |
Supporting (Hỗ trợ)
- Environment Record: Nơi lưu trữ bindings trong Lexical Environment (đã học ở 1.2.7).
- Creation Phase vs Execution Phase: Declaration thường xảy ra trong Creation Phase; initialization và assignment thường xảy ra trong Execution Phase (tùy loại khai báo).
Awareness (Biết tồn tại)
functiondeclaration có behavior đặc biệt (sẽ học ở 1.3.5).classdeclaration cũng có behavior riêng (sẽ học ở 1.3.5).
Out of Scope (Không học trong bài này)
- JIT compilation, hidden classes, engine bytecode.
- Chi tiết exact algorithm của InitializeBinding trong ECMASpec (chỉ dùng ở mức mental model).
6. Worked Example (Ví dụ phân tích từng bước)
Hãy trace đoạn code sau bằng mental model vòng đời biến:
var price = 100; // Một dòng code, nhưng engine xử lý qua 2 phase khác nhau
console.log(price);Step 1 — Creation Phase (Global Execution Context)
- Engine quét code, thấy
var price. - Declaration: Tạo binding
pricetrong Global Environment Record. - Initialization: Vì là
var, engine initializepricethànhundefinedngay trong Creation Phase.
Trạng thái Environment Record:
price: undefined (declared + initialized)Step 2 — Execution Phase
- Dòng 1:
price = 100— đây là Assignment. Giá trị được cập nhật từundefined→100. - Dòng 2:
console.log(price)— đọc binding, nhận được100.
Bây giờ so sánh với let:
console.log(discount);
let discount = 20;Step 1 — Creation Phase
- Engine thấy
let discount. - Declaration: Tạo binding
discounttrong Global Environment Record. - Initialization: Vì là
let, engine KHÔNG initialize trong Creation Phase. Binding ở trạng thái uninitialized.
Trạng thái Environment Record:
discount: <uninitialized> (declared but NOT initialized)Step 2 — Execution Phase
- Dòng 1:
console.log(discount)— engine cố gắng đọc binding chưa được initialize. - Kết quả:
ReferenceError: Cannot access 'discount' before initialization.
Code Review Lens
Khi review code, đừng hỏi "biến này có được hoisted không?" mà hãy hỏi: "Binding này đã được initialize chưa khi dòng này 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 tại sao.
console.log(a);
var a = 5;
console.log(a);[Đáp án & Giải thích]
Bạn nghĩ
- Dòng 1:
undefinedvìvarđược declare + initialize thànhundefinedtrong Creation Phase. - Dòng 3:
5vì assignment đã chạy.
- Dòng 1:
Giải thích
- Creation Phase:
adeclared, initialized toundefined. - Execution:
console.log(a)→undefined;a = 5→ assignment;console.log(a)→5.
- Creation Phase:
Prediction 2
Đừng chạy code. Dự đoán output hoặc error.
let b;
console.log(b);
b = 10;
console.log(b);[Đáp án & Giải thích]
Bạn nghĩ
- Dòng 2:
undefinedvìlet b;vẫn là initialization với giá trịundefined. - Dòng 4:
10sau assignment.
- Dòng 2:
Giải thích
let b;thực hiện cả declaration và initialization (với giá trịundefined) tại cùng một dòng trong Execution Phase.- Binding đã được initialize ngay khi dòng này chạy, nên không còn trong TDZ. Điểm then chốt:
letmà không có initializer vẫn initialize thànhundefined— khác biệt so vớiletvới giá trị explicit chỉ ở giá trị, không ở trạng thái uninitialized.
Prediction 3
Đừng chạy code. Dự đoán output hoặc error.
const c;
c = 3;
console.log(c);[Đáp án & Giải thích]
Bạn nghĩ
SyntaxErrorngay tại dòng 1 vìconstbắt buộc phải có initializer trong cùng câu khai báo.
Giải thích
constyêu cầu declaration + initialization + assignment xảy ra đồng thời. Bạn không thể táchconst c;vàc = 3;.- Đây là lý do
constkhông bao giờ có TDZ kéo dài qua nhiều dòng — nó bắt buộc initialize ngay lập tức.
8. Implementation Lab (Bài lab thực hành)
Level 1 — Guided (Hướng dẫn)
Viết lại đoạn code sau, tách rõ từng phase bằng comment:
// Phase: Declaration + Initialization + Assignment
let score = 0;
// Phase: Assignment
score = 100;Level 2 — Partial Scaffold (Khung mẫu một phần)
Hoàn thành comment mô tả trạng thái Environment Record sau từng dòng:
var total; // Declared? ___ Initialized? ___ Value: ___
total = 50; // Declared? ___ Initialized? ___ Value: ___
let count = 10; // Declared? ___ Initialized? ___ Value: ___Đáp án
var total; // Declared? Yes. Initialized? Yes. Value: undefined
total = 50; // Declared? Yes. Initialized? Yes. Value: 50
let count = 10; // Declared? Yes. Initialized? Yes. Value: 10Level 3 — Independent (Tự thực hiện)
Viết một đoạn code ngắn (tối đa 6 dòng) trong đó:
- Một biến
varđược log ra trước dòng khai báo và cho kết quảundefined. - Một biến
letđược log ra trước dòng khai báo và throwReferenceError. - Giải thích sự khác biệt bằng đúng 3 từ: declaration, initialization, assignment.
9. Edge Cases (Các trường hợp ngoại lệ)
Edge Case 1: let x; vs let x = undefined;
let a;
console.log(a); // undefined
let b = undefined;
console.log(b); // undefined// let a; → declare + initialize to undefined tại dòng này
// let b = ...; → declare + initialize to undefined tại dòng nàyCả hai đều cho undefined, nhưng mental model khác nhau ở chỗ:
let a;là initialization mặc định (không có explicit value).let b = undefined;là initialization explicit với biểu thức.
TIP
Trong production, let a; thường là code smell — nó báo hiệu bạn chưa biết giá trị ban đầu là gì, có thể dẫn đến logic rẽ nhánh phức tạp.
Edge Case 2: Re-declaration
var x = 1;
var x = 2; // Hợp lệ: declaration lặp lại, assignment mới
let y = 1;
let y = 2; // SyntaxError: Identifier 'y' has already been declaredvar cho phép declaration trùng tên vì nó chỉ cập nhật binding hiện có. let/const không cho phép vì binding đã tồn tại trong Environment Record.
Edge Case 3: TDZ bắt đầu từ đâu?
{
// TDZ của `value` bắt đầu ở đây, ngay đầu 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.
Prevention (Phòng ngừa):
- Không đọc biến trước dòng khai báo.
- Khi refactor
var→let, luôn kiểm tra xem có đoạn code nào đọc biến trước declaration không.
11. Design Exercise (Bài tập thiết kế giải pháp)
Depth L2–L3: Classification (Phân loại)
Cho 4 đoạn code sau, phân loại từng dòng thành D (Declaration), I (Initialization), A (Assignment), hoặc kết hợp (ví dụ D+I, I+A):
// Snippet A
var x = 10;
// Snippet B
let y;
y = 20;
// Snippet C
const z = 30;
// Snippet D
x = 40;[Đáp án tham khảo]
| Snippet | Phân loại | Giải thích |
|---|---|---|
| A | A (trong Execution Phase) | var x đã được D + I (thành undefined) trong Creation Phase. Dòng var x = 10 ở Execution Phase chỉ thực hiện Assignment (x = 10). |
| B dòng 1 | D + I | let y; declare và initialize thành undefined tại dòng này trong Execution Phase. |
| B dòng 2 | A | Gán giá trị mới. |
| C | D + I | const bắt buộc Declaration + Initialization xảy ra đồng thời tại dòng khai báo. Không có phase Assignment riêng biệt vì const không cho phép gán lại. |
| D | A | Chỉ là assignment, không có declaration. |
12. Production Scenario (Tình huống thực tế)
Bạn đang review một PR refactor legacy codebase:
// Trước
var API_KEY = "old-key";
function bootstrap() {
console.log(API_KEY);
var API_KEY = "new-key";
}
// Sau (developer đổi var → let)
let API_KEY = "old-key";
function bootstrap() {
console.log(API_KEY);
let API_KEY = "new-key"; // shadowing + TDZ
}Câu hỏi:
- Code trước in ra gì? Tại sao?
- Code sau in ra gì? Tại sao?
- Developer nghĩ gì sai về
letkhi họ refactor như vậy?
[Đáp án tham khảo]
- Code trước: In
undefined. Vìvar API_KEYtrong function được hoisted (declare + initializeundefined), che shadow biến global.console.logđọc localundefined. - Code sau: Throw
ReferenceError. Vìlet API_KEYtrong function được declare trong Creation Phase nhưng chưa initialize khiconsole.logchạy → TDZ. - Sai lầm: Developer nghĩ
let"không hoisted" nên sẽ đọc globalAPI_KEY. Thực tếletcó được đưa vào scope (declare), nhưng ở trạng thái uninitialized.
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ề declaration, initialization, và assignment. Dùng đúng terminology từ bài học này.
- Hỏi AI: "Explain the difference between declaration, initialization, and assignment in JavaScript. Use
varandletas examples." - So sánh: Câu trả lời AI có dùng từ "hoisted" để giải thích không? Nó có phân biệt rõ "binding is created" vs "binding is initialized" không?
- Verify: Kiểm tra lại bằng MDN hoặc ECMAScript specification (từ khóa:
Let and Const Declarations,CreateMutableBinding,InitializeBinding).
Gợi ý
Để ý xem AI có nói "let is not hoisted" không. Nếu có, đó là điểm mà mental model của bạn đã chính xác hơn AI. Binding của let được tạo trong Creation Phase, nhưng không được initialize.
Đáp án tham khảo
Bạn nghĩ
- Declaration = tạo tên trong Environment Record.
- Initialization = gắn giá trị hợp lệ đầu tiên (có thể là
undefined). - Assignment = thay đổi giá trị hiện có.
var: declare + initialize (undefined) trong Creation Phase; assignment trong Execution Phase.let: declare trong Creation Phase; initialize trong Execution Phase tại dòng khai báo.
AI trả lời (mô phỏng phản hồi thực tế)
- "Variables declared with
varare hoisted to the top of their scope and initialized withundefined." - "Variables declared with
letare hoisted but not initialized, so accessing them before the declaration results in a ReferenceError." - "Assignment happens when you use the
=operator."
- "Variables declared with
So sánh
- AI đúng ở bề mặt:
varđược initialize vớiundefined,letthrow error nếu truy cập sớm. - AI thiếu depth: Nó không giải thích rằng cả
varvàletđều có binding được tạo (declare) trong Creation Phase. Sự khác biệt nằm ở initialization, không phải ở việc "có được hoisted hay không". - AI thường dùng từ "hoisted" như một cơ chế duy nhất, trong khi đó chỉ là hệ quả của declaration phase.
- AI đúng ở bề mặt:
Điểm AI nói sai hoặc quá mơ hồ
- AI nói "
letis hoisted but not initialized" — câu này đúng nhưng dễ gây hiểu nhầm là có hai loại hoisting. Thực tế không phải "hoisting" là cơ chế; đó là hệ quả của việc engine chạy Creation Phase trước Execution Phase. - AI hiếm khi nhắc đến
constbắt buộc Declaration + Initialization xảy ra đồng thời và không cho phép assignment sau này.
- AI nói "
Kết luận
- Nếu bạn chỉ ra được rằng sự khác biệt cốt lõi là initialization timing chứ không phải "hoisting yes/no", bạn đã hiểu sâu hơn AI. Concept này sẽ quay lại ở bài 1.3.4 (TDZ) và 1.4 (Closure environment retention).
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
console.log(x)inundefinedkhixđược khai báo bằngvar, nhưng lại throwReferenceErrorkhi khai báo bằnglet? Dùng đúng 3 từ: declaration, initialization, assignment. Không được dùng từ 'hoisted'."
Mô phỏng
- Bạn nói
- "Khi JavaScript chạy một đoạn code, engine không chạy từ trên xuống ngay. Nó có hai phase: Creation Phase và Execution Phase."
- "Trong Creation Phase, engine nhìn qua code và tạo tất cả bindings trong Environment Record. Bước này gọi là declaration."
- "Với
var, engine không chỉ declare mà còn initialize binding đó ngay lập tức thànhundefined. Cho nên khi Execution Phase chạy đếnconsole.log(x),xđã có giá trị hợp lệ — dù làundefined." - "Với
let, 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, 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 nếu có giá trị). 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 declaration là việc đặt tên vào danh sách khách mời.
varlà 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).
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 |
|---|---|---|
| Phân biệt declaration, initialization, assignment | Classification (Phân loại) | L2 |
Giải thích behavior của var vs let trước dòng khai báo | Prediction (Dự đoán) + Explain (Giải thích) | L2–L3 |
| Trace trạng thái Environment Record | Implementation (Thực hành trace) | L3 |
Debug refactor var → let gây lỗi | Debug (Gỡ lỗi) | L3 |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể phân biệt declaration, initialization, và assignment trong một đoạn code JavaScript.
- [ ] Có thể giải thích tại sao
varcho phép truy cập trước dòng khai báo với giá trịundefined(đã được initialize trong Creation Phase). - [ ] Có thể giải thích tại sao
letthrowReferenceErrorkhi truy cập trước dòng khai báo (đã được declare nhưng chưa initialize). - [ ] Có thể trace trạng thái của Environment Record qua Creation Phase và Execution Phase cho script có tối đa 3 biến.
- [ ] 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): Execution Context (Creation/Execution Phase) + Scope & Lexical Environment → bạn đã biết engine chuẩn bị môi trường trước khi chạy code, và bindings nằm trong Environment Record.
Current (Hiện tại): Declaration vs Initialization vs Assignment → bạn hiểu rằng binding không xuất hiện trong một bước, và sự khác biệt giữa "tồn tại" (declared) và "dùng được" (initialized) là nền tảng của mọi behavior tiếp theo.
Next (Tiếp theo):
- 1.3.2 (
var): Xem cáchvarthực hiện declaration + initialization cùng lúc trong Creation Phase.- 1.3.3 (
let/const): Xem cáchlettách declaration và initialization.- 1.3.4 (TDZ): Hiểu chính xác Temporal Dead Zone là gì — nó chỉ là khoảng thời gian binding đã được declare nhưng chưa được initialize.
- 1.4 (Closure): Closure giữ reference đến Environment Record; để hiểu reference này giữ gì, bạn cần biết bindings trong đó đã ở phase nào.