Khái Niệm Về Hoisting Trong JavaScript

Hoisting nghĩa đen là “kéo lên”. Trong JavaScript, trước khi trình duyệt chạy code, trình thông dịch sẽ “kéo” các khai báo biến và hàm lên đầu phạm vi (scope).

Nói dễ hiểu hơn:

Bạn tưởng rằng code chạy từ trên xuống dưới, nhưng JavaScript trước tiên sẽ đi lén lút tìm hết các var, function rồi “đặt tạm” chúng lên trên đầu trong trí nhớ, rồi sau đó mới chạy code như bạn viết.

Ví dụ từng loại cho dễ hình dung

🟡 var – chỉ hoist phần khai báo, không hoist gán giá trị

console.log(a); // undefined
var a = 10;

⛳️ JavaScript hiểu thế này:

var a;         // "hoisted"
console.log(a); // undefined (vì chỉ mới khai báo, chưa gán giá trị)
a = 10;

⚠️ Không lỗi, nhưng sẽ ra undefined. Đây là lý do var được cho là “rủi ro cao” trong code lớn.

🔴 let / constcó hoist nhưng rơi vào vùng chết TDZ

console.log(b); // ❌ ReferenceError
let b = 20;

🧨 Lý do lỗi: Tồn tại vùng tên là Temporal Dead Zone (TDZ) – vùng từ đầu block tới lúc bạn khai báo biến.

🟢 function declaration – được hoist toàn phần

sayHello(); // ✅ "Hello"

function sayHello() {
  console.log("Hello");
}

⛳️ JavaScript hiểu thế này:

function sayHello() {
  console.log("Hello");
}
sayHello();

✅ Cả tên hàm và nội dung hàm đều được kéo lên đầu, nên gọi thoải mái trước khi khai báo.

🔴 function expressionKHÔNG hoist giá trị

greet(); // ❌ TypeError: greet is not a function

var greet = function () {
  console.log("Hi");
};

⛳️ JavaScript hiểu thế này:

var greet; // hoisted, giá trị là undefined
greet();   // ❌ Lỗi vì gọi undefined như function
greet = function () { ... }

So sánh tổng kết

LoạiCó Hoist?Gọi trước khai báo được không?Ghi nhớ đơn giản
var✅ Có✅ Được (giá trị là undefined)Cẩn thận vì không lỗi nhưng sai
let / const✅ Có❌ Không (TDZ gây lỗi)Phải khai báo trước mới dùng được
function✅ Có (đầy đủ)✅ ĐượcGọi thoải mái
Function expr.✅ Biến có❌ Không (gọi là lỗi)Chỉ dùng sau khi đã gán hàm

Kết luận dễ nhớ

  • var thì được hoist nhưng “nguy hiểm” → không nên dùng nữa.
  • let, const cũng hoist nhưng bị “khóa” trong TDZ.
  • function thì hoist hoàn toàn – có thể gọi thoải mái.
  • function expression chỉ hoist cái tên biến thôi, còn giá trị (hàm) thì chưa có → không gọi được trước.

Học để làm gì?

  1. Hiểu rõ hơn lỗi ReferenceError, TypeError khi code không chạy.
  2. Viết code an toàn hơn: Luôn khai báo trước khi dùng!
  3. Phân biệt rõ các loại hàm và biến.
  4. Tránh bug khó phát hiện trong code khi dùng var.

Ví dụ thực tế:

1. Hiểu rõ hơn lỗi ReferenceError, TypeError

Tình huống thực tế: Bạn làm một trang web và khi bấm nút thì báo lỗi Uncaught ReferenceError hoặc TypeError. Bạn không hiểu vì sao.

Ví dụ minh hoạ:

document.getElementById('btn').addEventListener('click', function () {
  console.log(message);
});

let message = "Xin chào!";

⛔️ Khi bạn bấm nút, lỗi hiện ra:

Uncaught ReferenceError: Cannot access 'message' before initialization

💡 Vì sao?

Biến message bị hoist, nhưng nằm trong TDZ do khai báo bằng let. Nó đã “tồn tại” nhưng bạn không được phép chạm vào trước dòng let message.

✅ Cách sửa đúng:

Khai báo trước khi dùng:

let message = "Xin chào!";
document.getElementById('btn').addEventListener('click', function () {
  console.log(message);
});

🎯 2. Viết code an toàn hơn – khai báo trước khi dùng

Tình huống thực tế: Một nhóm lập trình viên cùng viết code. Một người gọi biến count ở đầu file, người khác khai báo var count = 0; ở dưới cuối. Đoạn code chạy sai mà không ai để ý.

function showCount() {
  console.log(count); // ra undefined
}

showCount();

var count = 10;

Kết quả?

Bạn tưởng sẽ ra 10, nhưng thực tế ra undefined.

Vì var count được hoist lên trên đầu, nhưng phần gán = 10 thì không được hoist.

✅ Cách an toàn:

Khai báo rõ ràng trước khi dùng để tránh nhầm lẫn:

var count = 10;
function showCount() {
  console.log(count); // ✅ 10
}
showCount();

🎯 3. Phân biệt rõ các loại biến và hàm

Tình huống thực tế: Bạn đang viết một module có các function. Một cái dùng function, một cái dùng const = () =>. Bạn gọi nhầm thứ tự và bị lỗi.

Ví dụ minh họa:

sayHi();     // ✅ chạy được
sayHello();  // ❌ lỗi

function sayHi() {
  console.log("Chào bạn!");
}

const sayHello = function () {
  console.log("Hello!");
};

Kết quả:

sayHi() chạy bình thường vì đó là function declaration, được hoist hoàn toàn. sayHello()function expression, chỉ hoist cái tên biến sayHello, nhưng giá trị thì chưa được gán → TypeError.

🎯 4. Tránh bug khó phát hiện khi dùng var

Tình huống thực tế: Bạn dùng vòng lặp để gán sự kiện cho các nút. Nhưng nút nào cũng in ra số cuối cùng của vòng lặp!

for (var i = 0; i < 3; i++) {
  document.getElementById("btn" + i).addEventListener("click", function () {
    console.log("Bạn bấm nút số: ", i);
  });
}

⛔️ Dù bạn bấm btn0, btn1, hay btn2, tất cả đều in ra:

Bạn bấm nút số: 3

Vì var có phạm vi function scope, không phải block scope. Nên i bị hoist và dùng chung cho mọi hàm.

✅ Cách sửa đúng:

Dùng let để mỗi lần lặp tạo một bản sao riêng của i:

for (let i = 0; i < 3; i++) {
  document.getElementById("btn" + i).addEventListener("click", function () {
    console.log("Bạn bấm nút số: ", i); // đúng số
  });
}