Lesson 1.3.5 — Hoisting Is Not a Single Mechanism (Hoisting không phải một cơ chế duy nhất)
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.5 — Hoisting Is Not a Single Mechanism |
| Competency | C01.2 — Variables & Bindings |
| Depth | L3–L4 (Use → Debug) |
| Prerequisites | Lesson 1.3.1–1.3.4 (Declaration, var, let/const, TDZ) |
| Cognitive Load | High |
1. Why This Exists (Vì sao cần học)
Bạn đã học var, let, const, và TDZ như những khái niệm riêng lẻ. Giờ bạn cần một mental model thống nhất:
Không có cơ chế tên "hoisting" trong JavaScript engine.
Những gì developer gọi là "hoisting" thực chất là hệ quả của Creation Phase — engine quét code và tạo bindings trước khi chạy. Nhưng mỗi loại khai báo có timing khác nhau cho declaration và initialization:
function foo() {} // D + I (thành function object) trong Creation Phase
var x = 1; // D + I (thành undefined) trong Creation Phase
let y = 2; // D trong Creation Phase, I tại dòng khai báo
const z = 3; // D trong Creation Phase, I tại dòng khai báo
class Bar {} // D trong Creation Phase, I tại dòng khai báoNếu bạn nghĩ "tất cả đều được hoisted giống nhau, chỉ khác initialize", bạn sẽ không giải thích được tại sao function declaration có thể gọi trước dòng khai báo, tại sao class throw ReferenceError khi dùng trước, và tại sao var re-declare được còn let thì không.
Bài này tổng hợp tất cả loại khai báo thành một bảng so sánh duy nhất dựa trên environment model.
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 Declaration, Initialization, Assignment (Lesson 1.3.1).
- Hiểu
varđược D+I trong Creation Phase (Lesson 1.3.2). - Hiểu
let/constđược D trong Creation Phase, I tại dòng khai báo (Lesson 1.3.3). - Hiểu TDZ là trạng thái uninitialized (Lesson 1.3.4).
3. Learning Objectives (Mục tiêu học tập)
Sau bài này, bạn có thể:
- Phân loại 5 loại khai báo (
var,let,const,function,class) theo declaration timing và initialization timing. - Dự đoán behavior khi truy cập trước dòng khai báo cho mỗi loại.
- Giải thích tại sao "hoisting" không phải một cơ chế engine duy nhất.
- Debug code có mixed declarations bằng cách trace Environment Record.
4. Mental Model (Mô hình tư thu duy)
Mental Model: Creation Phase là "Scan & Prepare"
Engine đọc code
↓
Tạo Execution Context
↓
Quét toàn bộ declarations trong scope
↓
Tạo bindings trong Environment Record
↓
Khởi tạo (initialize) tùy loại khai báoMỗi loại khai báo có hợp đồng riêng với engine:
| Loại | Creation Phase | Execution Phase |
|---|---|---|
function | D + I (function object) | Không có gì thêm |
var | D + I (undefined) | Assignment nếu có |
let | D (uninitialized) | I + A tại dòng khai báo |
const | D (uninitialized) | I + A tại dòng khai báo |
class | D (uninitialized) | I tại dòng khai báo |
Sai lầm phổ biến
"Hoisting là cơ chế đưa khai báo lên đầu scope."
Sai. Không có cơ chế tên "hoisting". Engine không "di chuyển" code. Engine quét code trong Creation Phase và tạo bindings theo quy tắc riêng của từng loại khai báo.
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
| Concept | Định nghĩa |
|---|---|
| Function Declaration | function foo() {} — declare + initialize (thành function object) trong Creation Phase. |
var | Declare + initialize thành undefined trong Creation Phase. |
let/const | Declare trong Creation Phase, initialize tại dòng khai báo trong Execution Phase. |
class | Declare trong Creation Phase, initialize tại dòng khai báo (giống let/const). |
| No Hoisting Mechanism | "Hoisting" chỉ là tên gọi cho hệ quả của Creation Phase, không phải cơ chế engine. |
Supporting (Hỗ trợ)
- Function Expression (
const fn = function() {}) khác Function Declaration — nó làconstbinding, không được initialize sớm. - Re-declaration rules:
varcho phép,let/const/functioncấm trong cùng scope.
Awareness (Biết tồn tại)
importdeclarations cũng có behavior đặc biệt (sẽ học ở Stage 7).functiontrong block scope có behavior khác biệt giữa strict mode và non-strict mode.
Out of Scope (Không học trong bài này)
- Chi tiết spec algorithm của
InstantiateFunctionObject,CreatePerIterationEnvironment. - Block-scoped function behavior trong non-strict mode.
6. Worked Example (Ví dụ phân tích từng bước)
Trace đoạn code sau:
console.log(foo);
console.log(bar);
var foo = 1;
let bar = 2;
function foo() {}Step 1 — Creation Phase (Global Execution Context)
Engine quét toàn bộ script:
var foo→ declare + initializefootoundefined.let bar→ declarebar(uninitialized, TDZ).function foo() {}→ function declaration phát hiện. Bindingfoođã tồn tại (từvar foo), nhưng function declaration ghi đè initialize value thành function object.
Trạng thái Environment Record sau Creation Phase:
foo: [Function: foo] (từ var → bị ghi đè bởi function declaration)
bar: <uninitialized> (TDZ)Step 2 — Execution Phase
- Dòng 1:
console.log(foo)→ đọc bindingfoo→[Function: foo]. - Dòng 2:
console.log(bar)→barđang trong TDZ →ReferenceError: Cannot access 'bar' before initialization. - Dòng 3:
foo = 1→ assignment, ghi đè function object thành1. - Dòng 4:
bar = 2→ initialize + assignmentbar.
Code Review Lens
Khi thấy var và function cùng tên trong một scope, function declaration thắng trong Creation Phase. Nhưng sau đó var assignment có thể ghi đè lại. Đây là lý do không bao giờ nên dùng cùng tên cho var và function.
7. Prediction Exercise (Bài tập dự đoán)
Prediction 1
Đừng chạy code. Dự đoán output hoặc error cho từng dòng.
console.log(a);
var a = 10;
console.log(a);
function a() {}
console.log(a);[Đáp án & Giải thích]
Bạn nghĩ
- Dòng 1:
[Function: a]vì function declaration ghi đèvartrong Creation Phase. - Dòng 3:
10vì assignment ghi đè function object. - Dòng 5:
10vì assignment đã xảy ra.
- Dòng 1:
Giải thích
- Creation Phase:
var a→undefined;function a() {}→ ghi đè thành function object. - Execution:
console.log(a)→ function;a = 10→ assignment;console.log(a)→10.
- Creation Phase:
Prediction 2 — Transfer Exercise
Đừng chạy code. Dự đoán output hoặc error.
{
console.log(MyClass);
class MyClass {}
}[Đáp án & Giải thích]
Bạn nghĩ
ReferenceError: Cannot access 'MyClass' before initialization.
Giải thích
classdeclaration có TDZ giốnglet/const.- Binding được declare trong Creation Phase nhưng chưa initialize.
console.log(MyClass)chạy trong TDZ → throw error.
Prediction 3 — Transfer Exercise
Đừng chạy code. Dự đoán output hoặc error.
const fn = function () {
return 1;
};
console.log(fn());[Đáp án & Giải thích]
Bạn nghĩ
1.
Giải thích
- Đây là Function Expression, không phải Function Declaration.
const fnđược declare trong Creation Phase nhưng chưa initialize (TDZ).function() {}chỉ được evaluate tại dòng assignment trong Execution Phase.- Khác với
function fn() {}— function expression không được initialize sớm.
8. Implementation Lab (Bài lab thực hành)
Level 1 — Guided (Hướng dẫn)
Điền trạng thái Environment Record sau Creation Phase:
function demo() {
console.log(x);
var x = 1;
let y = 2;
function x() {}
}Trạng thái sau Creation Phase:
x: ___ (vì var + function declaration cùng tên)
y: ___ (vì let)Đáp án
x: [Function: x] (function declaration ghi đè var initialization)
y: <uninitialized> (TDZ)Level 2 — Partial Scaffold (Khung mẫu một phần)
Phân loại timing cho từng khai báo:
// A. function declaration
function greet() {}
// B. var
var message = "hi";
// C. let
let count = 0;
// D. const
const API = "/api";
// E. class
class User {}| Khai báo | Creation Phase | Execution Phase |
|---|---|---|
| A | ___ | ___ |
| B | ___ | ___ |
| C | ___ | ___ |
| D | ___ | ___ |
| E | ___ | ___ |
Đáp án
| Khai báo | Creation Phase | Execution Phase |
|---|---|---|
| A | D + I (function object) | Không có gì |
| B | D + I (undefined) | Assignment (message = "hi") |
| C | D (uninitialized) | I + A (count = 0) |
| D | D (uninitialized) | I + A (API = "/api") |
| E | D (uninitialized) | I (User = class object) |
Level 3 — Independent (Tự thực hiện)
Viết một script ngắn (tối đa 8 dòng) trong đó:
- Có ít nhất 3 loại khai báo khác nhau (
var,let,function). - Có ít nhất một dòng log trước dòng khai báo.
- Kết quả phụ thuộc vào việc function declaration ghi đè
var. - Giải thích bằng đúng 2 khái niệm: Creation Phase, Environment Record.
9. Edge Cases (Các trường hợp ngoại lệ)
Edge Case 1: Function Expression vs Function Declaration
console.log(fn); // [Function: fn]
function fn() {}console.log(fn); // ReferenceError!
const fn = function () {};Function Expression là biểu thức được evaluate tại Execution Phase. Dù nó tạo function object, binding fn vẫn là const/let/var và tuân theo rules của loại đó.
Edge Case 2: var và function cùng tên trong global scope
var alert = "not a function";
function alert() {}Trong browser, var alert tạo property trên window, nhưng function alert cũng tạo property. Function declaration thắng trong Creation Phase, nhưng var assignment sau đó có thể ghi đè. Đây là lý do không nên dùng tên trùng với global built-ins.
Edge Case 3: let trong switch block
switch (x) {
case 1:
let value = "one";
break;
case 2:
let value = "two"; // SyntaxError!
break;
}Toàn bộ switch body là một block scope. let value ở case 1 và case 2 là re-declaration trong cùng scope. Để fix, thêm block riêng cho mỗi case:
switch (x) {
case 1: {
let value = "one";
break;
}
case 2: {
let value = "two";
break;
}
}10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng): Code chạy trong development nhưng crash trong production build với ReferenceError.
// File: utils.js
console.log(formatDate(new Date()));
function formatDate(date) {
return date.toISOString();
}
// File: main.js (bundle bởi bundler)
import { formatDate } from "./utils.js";Sau khi bundle, main.js có thể trở thành:
console.log(formatDate(new Date()));
// ... nhiều dòng code khác ...
function formatDate(date) { ... }Reproduction (Tái hiện lỗi): Trong production bundle, formatDate không tồn tại khi console.log chạy.
Evidence (Bằng chứng):
- Lỗi:
ReferenceError: formatDate is not defined. - Code gốc dùng
functiondeclaration — vốn có thể gọi trước dòng khai báo. - Bundler chuyển
functionthànhconst formatDate = function() {}hoặc module wrapping.
Hypothesis (Giả thuyết):
- Trong development,
utils.jschạy như một script riêng lẻ.function formatDateđược declare và initialize trong global/script scope của file đó, nênconsole.logở đầu file vẫn resolve được. - Trong production bundle, code từ nhiều file được concat vào một module duy nhất.
console.log(formatDate(new Date()))từutils.jscó thể được đặt trong execution context khác (ví dụ: initialization function của module) trong khifunction formatDatenằm ở local scope của một wrapper khác, hoặc execution order thay đổi do tree-shaking/side-effect optimization. - Ngoài ra, dùng function trước declaration là anti-pattern — bundler không bảo đảm preserve execution order của side effects khi split/chunk code.
Verification (Xác minh): Di chuyển console.log xuống sau function declaration:
function formatDate(date) {
return date.toISOString();
}
console.log(formatDate(new Date()));Code chạy. Giả thuyết đúng.
Root Cause (Nguyên nhân gốc rễ): Developer dựa vào behavior "gọi function trước declaration" trong isolated script scope. Khi code được tích hợp vào hệ thống module lớn hơn, execution context và scope boundary thay đổi. Dù function declaration vẫn được hoisted trong cùng một scope, code gọi function có thể không còn trong cùng scope đó sau khi bundle.
Fix (Sửa): Luôn định nghĩa function trước khi gọi:
function formatDate(date) {
return date.toISOString();
}
console.log(formatDate(new Date()));Prevention (Phòng ngừa):
- ESLint rule
no-use-before-defineáp dụng cho cả function declarations. - Không dựa vào "hoisting" để sắp xếp code.
11. Design Exercise (Bài tập thiết kế giải pháp)
Depth L3–L4: Decision (Quyết định)
Bạn đang viết một utility module. Bạn cần một helper function calculate và một config constant MAX_RETRIES.
Option A:
var calculate = function (x) {
return x * 2;
};
const MAX_RETRIES = 3;Option B:
function calculate(x) {
return x * 2;
}
const MAX_RETRIES = 3;Câu hỏi:
- Option nào cho phép gọi
calculate()trước dòng khai báo? Tại sao? - Option nào an toàn hơn khi refactor? Tại sao?
- Bạn chọn option nào và lý do?
[Đáp án tham khảo]
Bạn nghĩ
- Option B cho phép gọi trước vì function declaration được D+I trong Creation Phase.
- Option A không cho phép vì
var calculatechỉ được D+I thànhundefinedtrong Creation Phase; function object chỉ được assign tại dòng code. - Option B an toàn hơn vì function declaration rõ ràng và không bị ghi đè bởi accidental assignment.
Giải thích
- Tuy nhiên, "gọi trước dòng khai báo" là anti-pattern. Cả hai option đều nên đặt declaration trước usage.
- Option B vẫn tốt hơn vì syntax rõ ràng, stack trace tốt hơn, và không tạo unnecessary TDZ risk.
12. Production Scenario (Tình huống thực tế)
Bạn review một PR trong đó developer dùng pattern "gọi trước, định nghĩa sau":
// main.js
setup();
configure();
function setup() {
/* ... */
}
function configure() {
/* ... */
}Developer biện minh: "Function declaration được hoisted, nên code vẫn chạy đúng."
Câu hỏi:
- Code có chạy không? Tại sao?
- Bạn có approve PR này không? Tại sao?
[Đáp án tham khảo]
Câu 1: Có chạy. Function declaration được D+I trong Creation Phase, nên
setupvàconfiguređã tồn tại khi dòng gọi chạy.Câu 2: Không approve. Lý do:
- Code khó đọc: reader phải cuộn xuống cuối file để tìm định nghĩa.
- Dễ gây lỗi khi refactor sang module/bundle.
- Violates principle of least surprise: developer khác sẽ không mong đợi function được gọi trước khi nhìn thấy nó.
13. AI-assisted Exercise (Bài tập với AI)
Level C — Delegate & Inspect (Giao phó & Kiểm tra)
- Tự trả lời trước: Viết ra giấy bảng so sánh 5 loại khai báo theo 2 cột: Creation Phase và Execution Phase.
- Hỏi AI: "Explain hoisting in JavaScript. Compare var, let, const, function, and class declarations."
- So sánh: AI có nói "hoisting is a mechanism" không? Nó có phân biệt rõ function declaration vs function expression không?
- Verify: Kiểm tra bằng MDN hoặc ECMAScript spec (từ khóa:
CreateGlobalFunctionBinding,LexicalEnvironment,VarEnvironment).
Gợi ý
Để ý xem AI có nói "all declarations are hoisted" không. Nếu có, đó là dấu hiệu mental model của AI đang dùng approximation. Bạn đã học rằng không có cơ chế tên "hoisting" — chỉ có Creation Phase với rules khác nhau cho từng loại khai báo.
Đáp án tham khảo
Bạn nghĩ
function: D + I (function object) trong Creation Phase.var: D + I (undefined) trong Creation Phase.let/const: D trong Creation Phase, I tại dòng khai báo.class: D trong Creation Phase, I tại dòng khai báo.- Không có "hoisting mechanism".
AI trả lời (mô phỏng phản hồi thực tế)
- "Hoisting is JavaScript's default behavior of moving declarations to the top of the current scope."
- "Function declarations are hoisted with their definition, while var is hoisted with undefined."
- "let and const are hoisted but not initialized, so they have a Temporal Dead Zone."
- "class declarations are also hoisted but not initialized."
So sánh
- AI đúng về behavior surface nhưng sai về mechanism. Nó dùng "hoisted" như cơ chế thực sự tồn tại.
- AI thường không phân biệt function declaration vs function expression.
- AI hiếm khi nhắc đến việc engine có hai Environment Records:
VarEnvironment(chovar) vàLexicalEnvironment(cholet/const/class).
Điểm AI nói sai hoặc quá mơ hồ
- AI nói "moving declarations to the top" — câu này gây hiểu nhầm engine di chuyển code. Thực tế code không di chuyển; engine chỉ quét và tạo bindings.
- AI nói "all declarations are hoisted" — gộp chung behavior khác nhau thành một cơ chế.
Kết luận
- Nếu bạn chỉ ra được rằng không có "hoisting mechanism", mà chỉ có Creation Phase scan với rules khác nhau cho từng loại khai báo, bạn đã hiểu sâu hơn AI. Concept này sẽ quay lại ở Stage 7 (Module system) và Stage 12 (Architecture).
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 nghe nói JavaScript có 'hoisting'. Vậy hoisting là gì? Có phải engine di chuyển code lên đầu file không? Giải thích bằng đúng terminology: Creation Phase, Environment Record, initialize. Không được dùng từ 'hoisted'."
Mô phỏng
- Bạn nói
- "JavaScript không có cơ chế tên 'hoisting'. Đó chỉ là tên gọi mà developer đặt cho một behavior."
- "Khi engine chạy code, nó làm hai phase: Creation Phase trước, Execution Phase sau."
- "Trong Creation Phase, engine quét code và tạo tất cả bindings trong Environment Record. Nhưng mỗi loại khai báo được xử lý khác nhau."
- "
function foo() {}được declare VÀ initialize thành function object ngay trong Creation Phase. Cho nên bạn có thể gọi nó trước dòng khai báo." - "
var xđược declare VÀ initialize thànhundefinedtrong Creation Phase. Cho nên bạn có thể đọc nó trước dòng khai báo — dù giá trị làundefined." - "
let ychỉ được declare trong Creation Phase, nhưng chưa initialize. Nó ở trạng thái uninitialized cho đến dòng khai báo. Truy cập sớm sẽ throwReferenceError." - "Vậy nên không phải engine 'di chuyển' code. Engine chỉ 'chuẩn bị' bindings trước khi chạy, và mỗi loại khai báo có cách chuẩn bị riêng."
💡 Liên tưởng: Tưởng tượng một buổi tiệc. Creation Phase là lúc nhân viên lập danh sách khách mời và chuẩn bị chỗ ngồi.
functionlà khách VIP được giao vé và chỗ ngồi ngay.varlà khách được giao vé tạm ghi 'TBD'.letlà khách có tên trong danh sách nhưng chưa có vé — nếu cố vào, bị đuổi ra. Không ai "di chuyển" lên đầu danh sách cả.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 và spec algorithm thay vì analogy.
Gợi ý đánh giá bản thân
- Đồng nghiệp có hiểu tại sao
functiongọi trước được cònletkhông? - Bạn có thể dự đoán behavior của
classmà không cần nhớ cú pháp không? - Nếu đồng nghiệp hỏi "vậy tại sao người ta gọi là hoisting?", bạn trả lời được không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Depth |
|---|---|---|
| Phân loại 5 loại khai báo theo Creation/Execution Phase | Classification (Phân loại) | L3 |
| Dự đoán behavior truy cập trước dòng khai báo | Prediction (Dự đoán) | L3 |
| Giải thích tại sao "hoisting" không phải cơ chế engine | Explain (Giải thích) + Teach Back | L2–L3 |
| Debug mixed declarations trong code | Debug (Gỡ lỗi) | L4 |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể phân loại
var,let,const,function,classtheo declaration timing và initialization timing. - [ ] Có thể dự đoán đúng output/error khi truy cập trước dòng khai báo cho mỗi loại.
- [ ] Có thể giải thích tại sao "hoisting" không phải một cơ chế engine duy nhất.
- [ ] Có thể phân biệt Function Declaration (initialize sớm) và Function Expression (TDZ).
- [ ] Có thể debug code có
varvàfunctioncùng tên trong một scope. - [ ] 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): TDZ → bạn đã biết TDZ là trạng thái uninitialized của binding, và
typeofbehavior khác nhau giữa undeclared và TDZ.
Current (Hiện tại): Hoisting không phải một cơ chế duy nhất → bạn hiểu rằng "hoisting" chỉ là tên gọi cho hệ quả của Creation Phase, và mỗi loại khai báo (
var,let,const,function,class) có declaration/initialization timing riêng.
Next (Tiếp theo):
- 1.3.6 (Hoisting trong Function): So sánh
varvàlettrong cùng một function body bằng environment model — bài tổng hợp trước khi sang Module 1.4.- 1.4 (Closure): Closure giữ reference đến Environment Record. Để hiểu closure giữ những binding nào và ở trạng thái nào, bạn cần biết chính xác Creation Phase tạo ra gì.
- Stage 7 (Modules):
importdeclarations cũng có behavior đặc biệt trong module graph — sẽ được xem xét lại trong context của module resolution.