Cơ chế hoạt động của Hoisting trong JavaScript

Javascript căn bản | by Học Javascript

Trong JavaScript, Hoisting là một cơ chế đặc biệt giúp trình thông dịch có thể "kéo" các khai báo biến và hàm lên đầu phạm vi trước khi thực thi mã. Điều này có nghĩa là bạn có thể sử dụng một biến hoặc một hàm trước khi nó được khai báo trong mã nguồn. Tuy nhiên, cơ chế này không hoạt động giống nhau đối với tất cả các loại khai báo, và nếu không hiểu rõ, bạn có thể gặp phải những lỗi khó đoán.

Vậy Hoisting hoạt động như thế nào? Sự khác biệt giữa Hoisting của var, let, const, Function Declaration và Function Expression là gì? Làm thế nào để tránh các lỗi phổ biến do Hoisting gây ra? Hãy cùng tìm hiểu chi tiết trong bài viết này!

Hoisting trong JavaScript là gì?

Hoisting là một cơ chế trong JavaScript mà trình thông dịch sẽ đưa các khai báo biến và hàm lên đầu phạm vi của chúng trước khi mã được thực thi. Điều này có nghĩa là bạn có thể sử dụng một biến hoặc một hàm trước khi nó được khai báo trong mã nguồn mà không gặp lỗi.

Tuy nhiên, hoisting không thực sự di chuyển code, mà chỉ giúp trình biên dịch xử lý các khai báo trước khi thực thi code.

Ảnh hưởng của Hoisting đến quá trình thực thi mã JavaScript

Hoisting ảnh hưởng đến biếnhàm theo các cách khác nhau:

  • Biến được khai báo bằng var: Được hoisting nhưng giá trị của chúng mặc định là undefined.
  • Biến được khai báo bằng letconst: Cũng được hoisting nhưng không thể sử dụng trước khi khai báo do "Temporal Dead Zone" (TDZ).
  • Hàm được khai báo bằng Function Declaration: Được hoisting hoàn toàn và có thể sử dụng trước khi khai báo.
  • Hàm được khai báo bằng Function Expression hoặc Arrow Function: Chỉ hoisting phần khai báo biến, nhưng không hoisting phần gán giá trị.

Ví dụ minh họa về Hoisting

Ví dụ 1: Hoisting với var

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

Giải thích:

  • JavaScript hoisting khai báo var a; lên đầu, nhưng không gán giá trị ngay lập tức, nên khi console.log(a); chạy lần đầu tiên, a có giá trị undefined.
  • Sau đó, a = 10; được gán giá trị nên lần console.log(a); thứ hai in ra 10.

Ví dụ 2: Hoisting với letconst

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;

Giải thích:

  • b vẫn được hoisting, nhưng nằm trong "Temporal Dead Zone" nên không thể truy cập trước khi khai báo.
  • Dùng letconst giúp tránh lỗi không mong muốn khi sử dụng biến trước khi khai báo.

Ví dụ 3: Hoisting với Function Declaration

sayHello(); // "Hello, world!"

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

Giải thích:

  • Hàm sayHello() được hoisting hoàn toàn, vì vậy nó có thể được gọi trước khi khai báo trong mã nguồn.

Ví dụ 4: Hoisting với Function Expression

sayHi(); // TypeError: sayHi is not a function

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

Giải thích:

  • var sayHi được hoisting, nhưng giá trị của nó (function) thì không.
  • Khi sayHi(); được gọi trước khi khai báo, sayHi có giá trị undefined, không phải là một hàm, dẫn đến lỗi.

Cơ chế Hoạt động của Hoisting trong JavaScript

Cách JavaScript xử lý Hoisting trong quá trình biên dịch

Trước khi một đoạn mã JavaScript được thực thi, nó trải qua hai giai đoạn chính:

Giai đoạn biên dịch (Compilation Phase)

  • Trình thông dịch JavaScript quét mã nguồn, tìm các khai báo biến và hàm và đưa chúng vào bộ nhớ.
  • Biến khai báo bằng var được gán giá trị mặc định là undefined.
  • Biến khai báo bằng letconst được đưa vào Temporal Dead Zone (TDZ) và không thể sử dụng trước khi khai báo.
  • Hàm khai báo bằng Function Declaration được lưu toàn bộ nội dung vào bộ nhớ và có thể sử dụng trước khi khai báo.

Giai đoạn thực thi (Execution Phase)

  • Chương trình chạy từ trên xuống dưới, gán giá trị cho biến và gọi hàm theo logic đã viết.

Sự khác biệt giữa Hoisting của biến và hàm

Loại Có bị hoisting không? Giá trị ban đầu Có thể gọi trước khi khai báo?
var undefined Không có lỗi nhưng giá trị là undefined
let Có (nhưng trong TDZ) Không có giá trị ban đầu Gây lỗi ReferenceError
const Có (nhưng trong TDZ) Không có giá trị ban đầu Gây lỗi ReferenceError
Function Declaration Toàn bộ hàm Có thể gọi trước khi khai báo
Function Expression (gán vào var) Có (chỉ khai báo) undefined Gây lỗi TypeError khi gọi trước

Hoisting với var

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

Giải thích:

  • JavaScript hoisting biến a, nên khi gọi console.log(a); trước khi khai báo, nó không gây lỗi nhưng trả về undefined.
  • Sau đó, a = 10; được gán giá trị, nên lần in thứ hai trả về 10.

Thực tế, mã trên được JavaScript hiểu như sau:

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

Hoisting với let và const

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;

Giải thích:

  • b bị hoisting nhưng nằm trong Temporal Dead Zone (TDZ), nên không thể truy cập trước khi khai báo.
  • Dùng let hoặc const giúp tránh lỗi không mong muốn khi sử dụng biến trước khi khai báo.

Thực tế, JavaScript hiểu đoạn mã trên như sau:

// TDZ bắt đầu từ đây
let b;
console.log(b); // ReferenceError
b = 20;

Hoisting với Function Declaration

sayHello(); // "Hello, world!"

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

Giải thích:

  • Hàm sayHello() được hoisting hoàn toàn, nên có thể gọi trước khi khai báo.
  • Điều này giúp bạn viết mã linh hoạt hơn khi tổ chức các hàm trong chương trình.

Thực tế, JavaScript hiểu đoạn mã trên như sau:

function sayHello() {
    console.log("Hello, world!");
}
sayHello(); // "Hello, world!"

Hoisting với Function Expression

sayHi(); // TypeError: sayHi is not a function

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

Giải thích:

  • var sayHi được hoisting, nhưng chỉ phần khai báo, không có giá trị hàm.
  • Khi gọi sayHi(); trước khi gán giá trị, sayHi có giá trị undefined, không phải là một hàm, nên gây lỗi TypeError.

Thực tế, JavaScript hiểu đoạn mã trên như sau:

var sayHi; // Hoisting chỉ đưa khai báo lên
sayHi(); // TypeError: sayHi is not a function
sayHi = function () {
    console.log("Hi!");
};

Hoisting với biến (var, let, const) trong JavaScript

Hoisting với var

  • Biến khai báo bằng var được hoisted lên đầu phạm vi nhưng có giá trị mặc định là undefined.
  • Điều này có nghĩa là bạn có thể sử dụng biến trước khi khai báo mà không gặp lỗi, nhưng giá trị của nó sẽ là undefined cho đến khi được gán giá trị.

Ví dụ minh họa:

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

Thực tế, JavaScript hiểu đoạn mã trên như sau:

var a; // Hoisting xảy ra, biến được khai báo nhưng chưa có giá trị
console.log(a); // undefined
a = 10;
console.log(a); // 10

Hoisting với let và const (Temporal Dead Zone - TDZ)

  • letconst cũng bị hoisted, nhưng chúng không được gán giá trị mặc định (undefined).
  • Nếu bạn truy cập vào chúng trước khi khai báo, chương trình sẽ gặp lỗi ReferenceError.
  • Đây là do Temporal Dead Zone (TDZ) – khoảng thời gian từ khi biến được hoisted đến khi được gán giá trị hợp lệ.

Ví dụ minh họa:

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;
console.log(b); // 20

Thực tế, JavaScript hiểu đoạn mã trên như sau:

// TDZ bắt đầu
let b; // Hoisting xảy ra, nhưng b chưa có giá trị
console.log(b); // ReferenceError
b = 20;
console.log(b); // 20

Sự khác biệt giữa let, constvar:

Loại biến Có bị hoisting? Giá trị mặc định Có thể truy cập trước khi khai báo?
var undefined Có (Nhưng là undefined)
let Không có Không (Gây ReferenceError)
const Không có Không (Gây ReferenceError)

Hoisting với hàm (Function Declaration & Function Expression) trong JavaScript

Hoisting với Function Declaration

  • Hàm khai báo bằng Function Declaration sẽ được hoisted hoàn toàn, nghĩa là bạn có thể gọi hàm trước khi khai báo mà không gặp lỗi.

Ví dụ minh họa:

sayHello(); // "Hello, world!"

function sayHello() {
    console.log("Hello, world!");
}
Thực tế, JavaScript hiểu đoạn mã trên như sau:
function sayHello() {
    console.log("Hello, world!");
}
sayHello(); // "Hello, world!"

Lưu ý: Đây là lợi thế của Function Declaration khi bạn cần gọi hàm trước khi định nghĩa nó.

Hoisting với Function Expression

  • Function Expression chỉ hoisting phần khai báo biến, nhưng không hoisting phần gán giá trị.
  • Nếu bạn gọi một Function Expression trước khi khai báo, chương trình sẽ gặp lỗi TypeError vì biến chỉ được khai báo mà chưa có giá trị là một hàm.

Ví dụ minh họa:

sayHi(); // TypeError: sayHi is not a function

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

Thực tế, JavaScript hiểu đoạn mã trên như sau:

var sayHi; // Biến được hoisting nhưng chưa gán giá trị
sayHi(); // TypeError: sayHi is not a function
sayHi = function () {
    console.log("Hi!");
};

Hoisting với Arrow Function

  • Arrow Function hoạt động giống như Function Expression, chỉ hoisting phần khai báo biến.
  • Nếu sử dụng var, bạn sẽ gặp lỗi TypeError.
  • Nếu sử dụng let hoặc const, bạn sẽ gặp lỗi ReferenceError vì bị ảnh hưởng bởi Temporal Dead Zone (TDZ).

Ví dụ minh họa:

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

var greet = () => {
    console.log("Hello!");
};

Thực tế, JavaScript hiểu đoạn mã trên như sau:

var greet; // Biến được hoisting nhưng chưa có giá trị
greet(); // TypeError: greet is not a function
greet = () => {
    console.log("Hello!");
};

Cách tránh lỗi và Best Practices khi làm việc với Hoisting trong JavaScript

Luôn khai báo biến ở đầu phạm vi

  • Để tránh nhầm lẫn về Hoisting, luôn khai báo biến ở đầu hàm hoặc đầu block.

Sử dụng letconst thay vì var

  • Giúp tránh lỗi từ TDZ và tránh việc biến có giá trị undefined.

Sử dụng Function Declaration nếu cần gọi trước khai báo

  • Nếu bạn cần gọi hàm trước khi khai báo, hãy dùng Function Declaration thay vì Function Expression.

Tránh sử dụng var để giảm rủi ro từ Hoisting

  • Sử dụng letconst để đảm bảo không có lỗi do khai báo muộn.

Ví dụ Best Practices

Code gây lỗi Hoisting

console.log(name); // undefined
var name = "Alice";

console.log(age); // ReferenceError
let age = 25;

greet(); // TypeError
var greet = function() {
    console.log("Hello!");
};

Code tốt hơn

let name = "Alice";
let age = 25;

function greet() {
    console.log("Hello!");
}

greet(); // "Hello!"
console.log(name); // "Alice"
console.log(age); // 25

Kết bài

Hoisting là một cơ chế quan trọng trong JavaScript, giúp các biến và hàm được "kéo lên" đầu phạm vi trước khi mã được thực thi. Hiểu rõ về hoisting giúp lập trình viên tránh được các lỗi phổ biến, đặc biệt là những vấn đề liên quan đến biến var, let, const và Function Expression.

Để viết mã hiệu quả và dễ bảo trì hơn, chúng ta nên:

  • Ưu tiên sử dụng let và const thay vì var để tránh lỗi từ Temporal Dead Zone (TDZ).
  • Sử dụng Function Declaration nếu cần gọi hàm trước khi khai báo, thay vì dùng Function Expression.
  • Khai báo biến ở đầu phạm vi để tránh lỗi hoisting ngoài ý muốn.

Hoisting có thể mang lại lợi ích nhưng cũng gây ra những lỗi khó đoán nếu không hiểu rõ cơ chế của nó. Vì vậy, nắm vững hoisting sẽ giúp bạn viết mã JavaScript tối ưu, dễ hiểu và ít lỗi hơn trong các dự án thực tế.

Bài viết liên quan