[Medium] 📄 Hoisting
1. What's Hoisting ?
Quá trình thực thi JS có thể được chia thành hai giai đoạn: giai đoạn tạo và giai đoạn thực thi:
var name = 'Pitt';
console.log(name); // print Pitt
Với đặc tính Hoisting, đoạn code trên cần được hiểu là: trước tiên khai báo biến, sau đó mới thực hiện gán giá trị.
// create
var name;
// execute
name = 'Pitt';
console.log(name);
Còn function thì khác với biến, nó được gán vào bộ nhớ ngay trong giai đoạn tạo. Khai báo hàm như sau:
getName();
function getName() {
console.log('string'); // print string
}
Đoạn code trên có thể chạy bình thường và in ra console.log mà không báo lỗi là nhờ logic sau: function được đưa lên trên cùng trước, sau đó mới thực hiện lệnh gọi function.
// create
function getName() {
console.log('string');
}
// execute
getName();
Tuy nhiên cần lưu ý rằng, với đặc tính Hoisting này, khi sử dụng biểu thức cần chú ý đến thứ tự viết code.
Trong giai đoạn tạo, function được ưu tiên cao nhất, tiếp theo mới là biến.
Correct
name = 'Yumy';
console.log(name); // print Yumy
var name;
// --- Equal to ---
// create
var name;
// execute
name = 'Yumy';
console.log(name); // print Yumy
Wrong
console.log(name); // print undefined
var name = 'Jane';
// --- Equal to ---
// create
var name;
// execute
console.log(name); // print undefined,vì chưa được gán giá trị, chỉ nhận được undefined mặc định
name = 'Pitt';
2. What's name printed ?
whoseName();
function whoseName() {
if (name) {
name = 'Nini';
}
}
var name = 'Pitt';
console.log(name);
Answer
// create
function whoseName() {
if (name) {
name = 'Nini';
}
}
var name;
// execute
whoseName();
name = 'Pitt';
console.log(name); // print Pitt
name trong whoseName() nhận được undefined nên không đi vào điều kiện.
Tuy nhiên, vì phía dưới khai báo hàm còn có một phép gán nữa, nên dù có vào điều kiện trong function đi chăng nữa, kết quả cuối cùng vẫn sẽ in ra Pitt.
3. Khai báo hàm vs Khai báo biến: Thứ tự ưu tiên Hoisting
Đề bài: Hàm và biến cùng tên
Hãy xác định kết quả đầu ra của đoạn code sau:
console.log(foo);
var foo = '1';
function foo() {}
Đáp án sai (hiểu nhầm phổ biến)
Nhiều người nghĩ rằng:
- Xuất ra
undefined(cho rằng var được đưa lên trước) - Xuất ra
'1'(cho rằng phép gán có ảnh hưởng) - Báo lỗi (cho rằng trùng tên sẽ xung đột)
Kết quả thực tế
[Function: foo]
Tại sao?
Câu hỏi này kiểm tra quy tắc ưu tiên của Hoisting:
Thứ tự ưu tiên Hoisting: Khai báo hàm > Khai báo biến
// Mã nguồn gốc
console.log(foo);
var foo = '1';
function foo() {}
// Tương đương với (sau khi Hoisting)
// Giai đoạn 1: Giai đoạn tạo (Hoisting)
function foo() {} // 1. Khai báo hàm được hoisting trước
var foo; // 2. Khai báo biến được hoisting (nhưng không ghi đè hàm đã tồn tại)
// Giai đoạn 2: Giai đoạn thực thi
console.log(foo); // Lúc này foo là hàm, xuất ra [Function: foo]
foo = '1'; // 3. Gán biến (ghi đè hàm)
Khái niệm then chốt
1. Khai báo hàm được đưa lên hoàn toàn
console.log(myFunc); // [Function: myFunc]
function myFunc() {
return 'Hello';
}
2. Khai báo biến var chỉ đưa lên phần khai báo, không đưa lên phần gán giá trị
console.log(myVar); // undefined
var myVar = 'Hello';
3. Khi khai báo hàm và khai báo biến cùng tên
// Thứ tự sau khi hoisting
function foo() {} // Hàm được hoisting và gán giá trị trước
var foo; // Khai báo biến được hoisting, nhưng không ghi đè hàm đã tồn tại
// Do đó foo là hàm
console.log(foo); // [Function: foo]
Luồng thực thi đầy đủ
// Mã nguồn gốc
console.log(foo); // ?
var foo = '1';
function foo() {}
console.log(foo); // ?
// ======== Tương đương với ========
// Giai đoạn tạo (Hoisting)
function foo() {} // 1️⃣ Khai báo hàm được hoisting (hoisting toàn bộ, bao gồm thân hàm)
var foo; // 2️⃣ Khai báo biến được hoisting (nhưng không ghi đè foo, vì đã là hàm rồi)
// Giai đoạn thực thi
console.log(foo); // [Function: foo] - foo là hàm
foo = '1'; // 3️⃣ Gán biến (lúc này mới ghi đè hàm)
console.log(foo); // '1' - foo trở thành chuỗi
Bài tập nâng cao
Bài tập A: Ảnh hưởng của thứ tự
console.log(foo); // ?
function foo() {}
var foo = '1';
console.log(foo); // ?
Đáp án:
[Function: foo] // Lần xuất đầu tiên
'1' // Lần xuất thứ hai
Lý do: Thứ tự code không ảnh hưởng đến kết quả Hoisting. Thứ tự ưu tiên đưa lên vẫn là: hàm > biến.
Bài tập B: Nhiều hàm cùng tên
console.log(foo); // ?
function foo() {
return 1;
}
var foo = '1';
function foo() {
return 2;
}
console.log(foo); // ?
Đáp án:
[Function: foo] { return 2; } // Lần xuất đầu tiên (hàm sau ghi đè hàm trước)
'1' // Lần xuất thứ hai (gán biến ghi đè hàm)
Lý do:
// Sau khi hoisting
function foo() {
return 1;
} // Hàm thứ nhất
function foo() {
return 2;
} // Hàm thứ hai ghi đè hàm thứ nhất
var foo; // Khai báo biến (không ghi đè hàm)
console.log(foo); // [Function: foo] { return 2; }
foo = '1'; // Gán biến (ghi đè hàm)
console.log(foo); // '1'
Bài tập C: Biểu thức hàm vs Khai báo hàm
console.log(foo); // ?
console.log(bar); // ?
var foo = function () {
return 1;
};
function bar() {
return 2;
}
Đáp án:
undefined; // foo là undefined
[Function: bar] // bar là hàm
Lý do:
// Sau khi hoisting
var foo; // Khai báo biến được hoisting (biểu thức hàm chỉ hoisting tên biến)
function bar() {
return 2;
} // Khai báo hàm được hoisting toàn bộ
console.log(foo); // undefined
console.log(bar); // [Function: bar]
foo = function () {
return 1;
}; // Gán biểu thức hàm
Điểm khác biệt then chốt:
- Khai báo hàm:
function foo() {}→ được đưa lên hoàn toàn (bao gồm thân hàm) - Biểu thức hàm:
var foo = function() {}→ chỉ tên biến được đưa lên, thân hàm không được đưa lên
let/const không gặp vấn đề này
// ❌ var có vấn đề hoisting
console.log(foo); // undefined
var foo = '1';
// ✅ let/const có TDZ (Vùng chết tạm thời)
console.log(bar); // ReferenceError: Cannot access 'bar' before initialization
let bar = '1';
// ✅ let/const trùng tên với hàm sẽ báo lỗi
function baz() {} // SyntaxError: Identifier 'baz' has already been declared
let baz = '1';
Tổng kết thứ tự ưu tiên Hoisting
Thứ tự ưu tiên Hoisting (từ cao đến thấp):
1. Khai báo hàm (Function Declaration)
├─ function foo() {} ✅ được đưa lên hoàn toàn
└─ ưu tiên cao nhất
2. Khai báo biến (Variable Declaration)
├─ var foo ⚠️ chỉ đưa lên phần khai báo, không đưa lên phần gán
└─ không ghi đè hàm đã tồn tại
3. Gán giá trị biến (Variable Assignment)
├─ foo = '1' ✅ sẽ ghi đè hàm
└─ xảy ra trong giai đoạn thực thi
4. Biểu thức hàm (Function Expression)
├─ var foo = function() {} ⚠️ được coi là gán giá trị biến
└─ chỉ tên biến được đưa lên, thân hàm không được đưa lên
Trọng điểm phỏng vấn
Khi trả lời loại câu hỏi này, nên:
- Giải thích cơ chế Hoisting: Chia thành giai đoạn tạo và giai đoạn thực thi
- Nhấn mạnh thứ tự ưu tiên: Khai báo hàm > Khai báo biến
- Vẽ ra code sau khi Hoisting: Cho người phỏng vấn thấy sự hiểu biết của bạn
- Đề cập đến best practice: Sử dụng let/const, tránh vấn đề Hoisting của var
Ví dụ trả lời phỏng vấn:
"Câu hỏi này kiểm tra thứ tự ưu tiên của Hoisting. Trong JavaScript, khai báo hàm có thứ tự ưu tiên đưa lên cao hơn khai báo biến.
Quá trình thực thi chia thành hai giai đoạn:
- Giai đoạn tạo:
function foo() {}được đưa lên hoàn toàn lên trên cùng, tiếp theo khai báovar foođược đưa lên nhưng không ghi đè hàm đã tồn tại.- Giai đoạn thực thi: Tại
console.log(foo), lúc này foo là hàm nên xuất ra[Function: foo]. Sau đófoo = '1'mới ghi đè foo thành chuỗi.Best practice là sử dụng
let/constthay thếvar, và đặt khai báo hàm lên trên cùng để tránh loại nhầm lẫn này."