Hoisting Trong JavaScript: Cơ Chế Bí Ẩn Mà Mọi Dev Đều Phải Hiểu

Bạn từng thấy code JavaScript gọi một hàm ở dòng trên cùng, trong khi định nghĩa hàm đó lại nằm tận cuối file — nhưng chương trình vẫn chạy bình thường? Hay bạn truy cập một biến trước khi khai báo và nhận được undefined thay vì báo lỗi? Đó là dấu hiệu của Hoisting. Nắm hiểu cơ chế này không chỉ giúp bạn xử lý tốt các câu hỏi phỏng vấn kỹ thuật mà còn là chìa khóa để hiểu cách JavaScript engine thực sự hoạt động bên dưới — trước khi thực thi bất kỳ dòng code nào.

hoistingJavaScriptvarletconstfunction
Ảnh bìa bài viết: Hoisting Trong JavaScript: Cơ Chế Bí Ẩn Mà Mọi Dev Đều Phải Hiểu
Ảnh đại diện của Trung Vũ Hoàng

Trung Vũ Hoàng

Tác giả

29/3/20265 phút đọc

1. Hoisting Thực Sự Là Gì?

Nhiều tài liệu mô tả Hoisting là "đưa khai báo lên đầu file". Đây là cách diễn đạt dễ hiểu nhưng không hoàn toàn chính xác — không có dòng code nào thực sự bị di chuyển. Điều thực sự xảy ra là: trong giai đoạn khởi tạo (Creation Phase), trình thông dịch JavaScript quét qua mã nguồn, tìm tất cả khai báo biến và hàm, rồi cấp phát bộ nhớ cho chúng — trước khi bắt đầu thực thi.

Điều này tạo ra "ảo giác" rằng mọi khai báo đều sẵn sàng từ đầu phạm vi (scope). Nếu không hiểu rõ điều này, bạn rất dễ gặp các lỗi logic âm thầm — đặc biệt trong codebase nhiều người cùng làm việc với phong cách viết code khác nhau.

2. var, let, const — Hoisting Hoạt Động Khác Nhau Như Thế Nào?

Đây là điểm mấu chốt cần phân biệt rõ:

var — Hoisted và khởi tạo sẵn undefined

Khi khai báo với var, biến được hoisted được gán giá trị mặc định là undefined ngay từ đầu. Bạn có thể truy cập biến trước dòng khai báo mà không bị crash — nhưng nhận về undefined, dễ gây nhầm lẫn.

let và const — Hoisted nhưng vào Temporal Dead Zone (TDZ)

letconst (ES6+) cũng được hoisted, nhưng không được khởi tạo giá trị. Chúng rơi vào trạng thái gọi là Temporal Dead Zone (TDZ) — khoảng thời gian từ khi block bắt đầu cho đến khi dòng khai báo được thực thi. Truy cập biến trong vùng này sẽ ném ra ReferenceError ngay lập tức.

Đây chính là lý do letconst được coi là "an toàn hơn" — chúng buộc bạn phải khai báo trước khi dùng, làm code minh bạch và dễ đọc hơn hẳn.

3. Hoisting Với Hàm: Function Declaration vs Expression

Function Declaration — Hoisted hoàn toàn

Hàm khai báo theo cú pháp truyền thống (function tenHam() {}) được hoisted toàn bộ — cả tên lẫn nội dung hàm đều vào bộ nhớ ngay từ đầu. Bạn có thể gọi hàm từ bất kỳ đâu trong scope, kể cả trước dòng định nghĩa.

Function Expression — Theo quy tắc của biến

Nếu gán hàm vào biến (const fn = function() {} hoặc var fn = function() {}), quy tắc hoisting của biến đó được áp dụng. Với var, biến sẽ là undefined và gọi nó sớm sẽ gây lỗi "is not a function". Với const, sẽ nhận ReferenceError.

4. Tại Sao JavaScript Lại Có Cơ Chế Này?

Hoisting không phải là một thiếu sót ngẫu nhiên — nó có lý do thiết kế rõ ràng. Tính năng quan trọng nhất: hỗ trợ đệ quy tương hỗ (mutual recursion). Nếu hàm A gọi hàm B và hàm B gọi lại hàm A, cả hai cần "biết" về sự tồn tại của nhau. Hoisting cho phép điều đó bất kể thứ tự khai báo trong file.

Bên cạnh đó, nó phản ánh triết lý ban đầu của JavaScript: cố gắng chạy được thay vì dừng lại ngay khi gặp sự cố nhỏ. Tuy nhiên trong lập trình hiện đại, chúng ta ưu tiên sự kỷ luật và tường minh hơn là sự "linh hoạt" dễ gây lỗi này.

5. Ví Dụ Thực Tế và Phân Tích Lỗi

console.log(name); // undefined — không phải lỗi!
var name = "JavaScript";
console.log(name); // "JavaScript"

Trình thông dịch thực sự xử lý như sau: var name; được hoisted lên đầu (giá trị mặc định undefined), sau đó console.log đầu tiên chạy và thấy undefined, cuối cùng mới đến dòng gán giá trị. Thay var bằng let sẽ gây ReferenceError ngay dòng đầu.

Một cạm bẫy phổ biến khác: var không có block scope. Biến khai báo bằng var bên trong if hay for sẽ "thoát" ra ngoài block, tồn tại ở phạm vi hàm hoặc toàn cục — nguyên nhân của vô số bug trong các vòng lặp xử lý bất đồng bộ.

6. Best Practices — Sống Tốt Với Hoisting

  • Quên var đi, dùng let và const: Đây là quy tắc số một. const cho những gì không cần gán lại, let cho phần còn lại.

  • Khai báo biến ở đầu scope: Dù đã dùng let/const, thói quen khai báo biến ở đầu hàm giúp người đọc code dễ nắm bắt toàn bộ "nguyên liệu" của một logic.

  • Tránh đặt tên biến trùng tên hàm: Đây là nguồn gốc của những bug khó hiểu và khó tái tạo nhất.

  • Bật Strict Mode: Thêm "use strict"; ở đầu file để ngăn JavaScript tự động tạo biến toàn cục khi bạn quên từ khóa khai báo.

Liên hệ với Closure: Hoisting là bước chuẩn bị cho Lexical Environment — môi trường mà Closure sẽ "đóng gói" và ghi nhớ sau này. Hiểu Hoisting là bước đệm tự nhiên để hiểu sâu Closure.

7. Tổng Kết

Hoisting không phải là lỗi thiết kế — đó là một tính năng có chủ đích. Khi bạn hiểu rõ giai đoạn Creation Phase và Execution Phase, bạn hiểu thực sự JavaScript vận hành như thế nào — không phải qua những giải thích bề mặt.

Code sạch không phải là code sử dụng những mẹo hóc búa. Đó là code mà bất kỳ đồng nghiệp nào cũng đọc và hiểu được ngay lập tức. Hãy dùng kiến thức về Hoisting để viết code tường minh hơn — không phải để che giấu sự phức tạp.

Bạn thấy bài viết hữu ích?

Liên hệ với chúng tôi để được tư vấn miễn phí về dịch vụ

Liên hệ ngay

Bài viết liên quan