Skip to content

Lesson 1.3.5 — Hoisting Is Not a Single Mechanism (Hoisting không phải một cơ chế duy nhất) ​

Bài 1.3.5 — Hoisting Mechanism
Declaration Processing, Bindings & Hoisting Mental Model • 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.5 — Hoisting Is Not a Single Mechanism
CompetencyC01.2 — Variables & Bindings
DepthL3–L4 (Use → Debug)
PrerequisitesLesson 1.3.1–1.3.4 (Declaration, var, let/const, TDZ)
Cognitive LoadHigh

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:

text
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áo

Nế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ể:

  1. Phân loại 5 loại khai báo (var, let, const, function, class) theo declaration timing và initialization timing.
  2. Dự đoán behavior khi truy cập trước dòng khai báo cho mỗi loại.
  3. Giải thích tại sao "hoisting" không phải một cơ chế engine duy nhất.
  4. 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áo

Mỗi loại khai báo có hợp đồng riêng với engine:

LoạiCreation PhaseExecution Phase
functionD + I (function object)Không có gì thêm
varD + I (undefined)Assignment nếu có
letD (uninitialized)I + A tại dòng khai báo
constD (uninitialized)I + A tại dòng khai báo
classD (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 Declarationfunction foo() {} — declare + initialize (thành function object) trong Creation Phase.
varDeclare + initialize thành undefined trong Creation Phase.
let/constDeclare trong Creation Phase, initialize tại dòng khai báo trong Execution Phase.
classDeclare 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à const binding, không được initialize sớm.
  • Re-declaration rules: var cho phép, let/const/function cấm trong cùng scope.

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

  • import declarations cũng có behavior đặc biệt (sẽ học ở Stage 7).
  • function trong 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:

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

  1. var foo → declare + initialize foo to undefined.
  2. let bar → declare bar (uninitialized, TDZ).
  3. function foo() {} → function declaration phát hiện. Binding foo đã 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:

text
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 binding foo → [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ành 1.
  • Dòng 4: bar = 2 → initialize + assignment bar.

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.

js
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 đè var trong Creation Phase.
    • Dòng 3: 10 vì assignment ghi đè function object.
    • Dòng 5: 10 vì assignment đã xảy ra.
  • 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.

Prediction 2 — Transfer Exercise

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

js
{
  console.log(MyClass);
  class MyClass {}
}
[Đáp án & Giải thích]
  • Bạn nghĩ

    • ReferenceError: Cannot access 'MyClass' before initialization.
  • Giải thích

    • class declaration có TDZ giống let/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.

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

js
function demo() {
  console.log(x);
  var x = 1;
  let y = 2;
  function x() {}
}

Trạng thái sau Creation Phase:

text
x: ___ (vì var + function declaration cùng tên)
y: ___ (vì let)
Đáp án
text
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:

js
// 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áoCreation PhaseExecution Phase
A______
B______
C______
D______
E______
Đáp án
Khai báoCreation PhaseExecution Phase
AD + I (function object)Không có gì
BD + I (undefined)Assignment (message = "hi")
CD (uninitialized)I + A (count = 0)
DD (uninitialized)I + A (API = "/api")
ED (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 đó:

  1. Có ít nhất 3 loại khai báo khác nhau (var, let, function).
  2. Có ít nhất một dòng log trước dòng khai báo.
  3. Kết quả phụ thuộc vào việc function declaration ghi đè var.
  4. 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 ​

js
console.log(fn); // [Function: fn]
function fn() {}
js
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 ​

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

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

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

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

js
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 function declaration — vốn có thể gọi trước dòng khai báo.
  • Bundler chuyển function thành const formatDate = function() {} hoặc module wrapping.

Hypothesis (Giả thuyết):

  • Trong development, utils.js chạy như một script riêng lẻ. function formatDate được declare và initialize trong global/script scope của file đó, nên console.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.js có thể được đặt trong execution context khác (ví dụ: initialization function của module) trong khi function formatDate nằ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:

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

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

js
var calculate = function (x) {
  return x * 2;
};
const MAX_RETRIES = 3;

Option B:

js
function calculate(x) {
  return x * 2;
}
const MAX_RETRIES = 3;

Câu hỏi:

  1. Option nào cho phép gọi calculate() trước dòng khai báo? Tại sao?
  2. Option nào an toàn hơn khi refactor? Tại sao?
  3. 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 calculate chỉ được D+I thành undefined trong 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":

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

  1. Code có chạy không? Tại sao?
  2. 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 setup và 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)

  1. 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.
  2. Hỏi AI: "Explain hoisting in JavaScript. Compare var, let, const, function, and class declarations."
  3. 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?
  4. 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 (cho var) và LexicalEnvironment (cho let/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ành undefined trong Creation Phase. Cho nên bạn có thể đọc nó trước dòng khai báo — dù giá trị là undefined."
    • "let y chỉ đượ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ẽ throw ReferenceError."
    • "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. function là khách VIP được giao vé và chỗ ngồi ngay. var là khách được giao vé tạm ghi 'TBD'. let là 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 function gọi trước được còn let không?
  • Bạn có thể dự đoán behavior của class mà 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 PhaseClassification (Phân loại)L3
Dự đoán behavior truy cập trước dòng khai báoPrediction (Dự đoán)L3
Giải thích tại sao "hoisting" không phải cơ chế engineExplain (Giải thích) + Teach BackL2–L3
Debug mixed declarations trong codeDebug (Gỡ lỗi)L4

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

  • [ ] Có thể phân loại var, let, const, function, class theo 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ó var và function cù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à typeof behavior 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 var và let trong 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): import declarations cũng có behavior đặc biệt trong module graph — sẽ được xem xét lại trong context của module resolution.
📴 Offline Mode — Content served from cache