Skip to content

Lesson 1.3.1 — Declaration vs Initialization (Khai báo vs Khởi tạo) ​

Bài 1.3.1 — Declaration vs Initialization
Binding Creation, Initialization & Assignment • 27 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.1 — Declaration vs Initialization
CompetencyC01.2 — Variables & Bindings
DepthL2–L3 (Explain → Use)
PrerequisitesExecution Context (1.1), Scope & Lexical Environment (1.2), familiarity with let/const/var syntax
Cognitive LoadMedium

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:

js
console.log(user); // undefined (với var)
// ...
let user = "Alice"; // ReferenceError nếu đổi sang let

Nhiều developer giải thích hiện tượng này bằng câu:

"Vì var được hoisted còn let thì 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:

js
const config;
config = { api: "https://api.example.com" }; // SyntaxError

Bà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ể:

  1. Phân biệt declaration, initialization, và assignment trong một đoạn code JavaScript.
  2. Giải thích tại sao var cho phép truy cập trước dòng khai báo (với giá trị undefined) trong khi let/const lại throw error.
  3. Trace trạng thái của Environment Record qua từng phase của vòng đời biến.
  4. 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à undefined hoặ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
DeclarationTạo identifier binding trong Environment Record của lexical scope.
InitializationGắn giá trị ban đầu hợp lệ vào binding (có thể là undefined).
AssignmentThay thế giá trị hiện tại của binding bằng giá trị mới.
Uninitialized bindingBinding đã đượ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) ​

  • function declaration có behavior đặc biệt (sẽ học ở 1.3.5).
  • class declaration 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:

js
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 price trong Global Environment Record.
  • Initialization: Vì là var, engine initialize price thành undefined ngay trong Creation Phase.

Trạng thái Environment Record:

text
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 được 100.

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

js
console.log(discount);
let discount = 20;

Step 1 — Creation Phase

  • Engine thấy let discount.
  • Declaration: Tạo binding discount trong 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:

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

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

    • Dòng 1: undefined vì var được declare + initialize thành undefined trong Creation Phase.
    • Dòng 3: 5 vì assignment đã chạy.
  • Giải thích

    • Creation Phase: a declared, initialized to undefined.
    • Execution: console.log(a) → undefined; a = 5 → assignment; console.log(a) → 5.

Prediction 2

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

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

    • Dòng 2: undefined vì let b; vẫn là initialization với giá trị undefined.
    • Dòng 4: 10 sau assignment.
  • 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: let mà không có initializer vẫn initialize thành undefined — khác biệt so với let vớ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.

js
const c;
c = 3;
console.log(c);
[Đáp án & Giải thích]
  • Bạn nghĩ

    • SyntaxError ngay tại dòng 1 vì const bắt buộc phải có initializer trong cùng câu khai báo.
  • Giải thích

    • const yêu cầu declaration + initialization + assignment xảy ra đồng thời. Bạn không thể tách const c; và c = 3;.
    • Đây là lý do const khô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:

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

js
var total; // Declared? ___ Initialized? ___ Value: ___

total = 50; // Declared? ___ Initialized? ___ Value: ___

let count = 10; // Declared? ___ Initialized? ___ Value: ___
Đáp án
js
var total; // Declared? Yes. Initialized? Yes. Value: undefined
total = 50; // Declared? Yes. Initialized? Yes. Value: 50
let count = 10; // Declared? Yes. Initialized? Yes. Value: 10

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

Viết một đoạn code ngắn (tối đa 6 dòng) trong đó:

  1. Một biến var được log ra trước dòng khai báo và cho kết quả undefined.
  2. Một biến let được log ra trước dòng khai báo và throw ReferenceError.
  3. 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; ​

js
let a;
console.log(a); // undefined

let b = undefined;
console.log(b); // undefined
js
// let a;        → declare + initialize to undefined tại dòng này
// let b = ...;  → declare + initialize to undefined tại dòng này

Cả 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 ​

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

var 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? ​

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

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.

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

js
// 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]
SnippetPhân loạiGiải thích
AA (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 1D + Ilet y; declare và initialize thành undefined tại dòng này trong Execution Phase.
B dòng 2AGán giá trị mới.
CD + Iconst 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.
DAChỉ 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:

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

  1. Code trước in ra gì? Tại sao?
  2. Code sau in ra gì? Tại sao?
  3. Developer nghĩ gì sai về let khi họ refactor như vậy?
[Đáp án tham khảo]
  • Code trước: In undefined. Vì var API_KEY trong function được hoisted (declare + initialize undefined), che shadow biến global. console.log đọc local undefined.
  • Code sau: Throw ReferenceError. Vì let API_KEY trong function được declare trong Creation Phase nhưng chưa initialize khi console.log chạy → TDZ.
  • Sai lầm: Developer nghĩ let "không hoisted" nên sẽ đọc global API_KEY. Thực tế let có đượ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)

  1. 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.
  2. Hỏi AI: "Explain the difference between declaration, initialization, and assignment in JavaScript. Use var and let as examples."
  3. 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?
  4. 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 var are hoisted to the top of their scope and initialized with undefined."
    • "Variables declared with let are hoisted but not initialized, so accessing them before the declaration results in a ReferenceError."
    • "Assignment happens when you use the = operator."
  • So sánh

    • AI đúng ở bề mặt: var được initialize với undefined, let throw error nếu truy cập sớm.
    • AI thiếu depth: Nó không giải thích rằng cả var và 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.
  • Điểm AI nói sai hoặc quá mơ hồ

    • AI nói "let is 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 const bắt buộc Declaration + Initialization xảy ra đồng thời và không cho phép assignment sau này.
  • 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) in undefined khi x được khai báo bằng var, nhưng lại throw ReferenceError khi khai báo bằng let? 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ành undefined. Cho nên khi Execution Phase chạy đến console.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 → throw ReferenceError."
    • "Dòng let x = 5 sau đó mới thực hiện initialization (và assignment nếu có giá trị). 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 declaration là việc đặt tên vào danh sách khách mời. var là 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).

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
Phân biệt declaration, initialization, assignmentClassification (Phân loại)L2
Giải thích behavior của var vs let trước dòng khai báoPrediction (Dự đoán) + Explain (Giải thích)L2–L3
Trace trạng thái Environment RecordImplementation (Thực hành trace)L3
Debug refactor var → let gây lỗiDebug (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 var cho 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 let throw ReferenceError khi 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ách var thực hiện declaration + initialization cùng lúc trong Creation Phase.
  • 1.3.3 (let/const): Xem cách let tá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.
📴 Offline Mode — Content served from cache