Đã bao giờ bạn tự hỏi tại sao có thể gọi một hàm JavaScript trước khi định nghĩa, nhưng lại nhận ngay lỗi khi làm điều tương tự với một biến let? Những hành vi tưởng chừng ‘kỳ lạ’ này không phải là lỗi, mà là kết quả của hai khái niệm nền tảng: Hoisting và Scope. Hiểu rõ cách chúng tương tác với nhau chính là chìa khóa để bạn làm chủ luồng thực thi của code. Bài viết này của WiWeb sẽ giúp bạn tìm hiểu tường tận hai ‘nhân vật’ quyền lực này một cách đơn giản và trực quan nhất.
Scope trong Javascript là gì?
Hãy tưởng tượng Scope như những bức tường của một ngôi nhà. Nó quy định xem từ một căn phòng cụ thể, bạn có thể ‘nhìn thấy’ và sử dụng những đồ vật (biến, hàm) nào trong các phòng khác hay ngoài sân (toàn cục). Nói một cách kỹ thuật, Scope là cơ chế xác định phạm vi truy cập của các biến và hàm trong mã của bạn. Việc này giúp tránh xung đột tên và bảo vệ dữ liệu, tạo nên một cấu trúc code có tổ chức.
Định nghĩa Scope: Quyết định sự “nhìn thấy” của biến và hàm
Khi bạn khai báo một biến, nó không tồn tại ở mọi nơi trong chương trình. Nó chỉ ‘sống’ và có thể được truy cập bên trong một Scope nhất định. Nếu bạn cố gắng gọi một biến từ bên ngoài Scope của nó, JavaScript sẽ thẳng thừng báo lỗi ReferenceError. Đây là một tính năng, không phải là một lỗi. Nó giúp chúng ta tạo ra các thành phần độc lập, tránh việc vô tình thay đổi giá trị của một biến ở một nơi hoàn toàn không liên quan.
Global Scope (Phạm vi toàn cục)
Bất cứ biến hay hàm nào được khai báo bên ngoài tất cả các hàm và các khối lệnh {} đều thuộc về Global Scope. Giống như không khí vậy, mọi đoạn code trong ứng dụng của bạn đều có thể truy cập và thay đổi chúng.
// 'websiteName' nằm trong Global Scope
var websiteName = "WiWeb.vn";
function showWebsite() {
console.log(websiteName); // Truy cập được vì nó là global
}
showWebsite(); // In ra "WiWeb.vn"
Tiện lợi là vậy, nhưng Global Scope lại là con dao hai lưỡi. Việc lạm dụng biến toàn cục có thể dẫn đến ‘ô nhiễm’ phạm vi (scope pollution), khi nhiều phần của chương trình cùng sửa đổi một biến, gây ra các lỗi khó lường và khiến việc gỡ rối trở thành một cơn ác mộng.
Function Scope (Phạm vi hàm)
Đây là quy tắc truyền thống của JavaScript trước khi ES6 ra đời. Bất kỳ biến nào được khai báo bằng từ khóa var bên trong một hàm sẽ chỉ có thể được truy cập từ bên trong hàm đó. Nó hoàn toàn vô hình với thế giới bên ngoài.
function greet() {
var message = "Chào mừng đến với WiWeb!"; // 'message' có Function Scope
console.log(message);
}
greet(); // In ra "Chào mừng đến với WiWeb!"
console.log(message); // Lỗi! ReferenceError: message is not defined
Điều này tạo ra một không gian riêng tư, an toàn cho mỗi hàm hoạt động mà không sợ ảnh hưởng đến các phần khác.
Block Scope
ES6 (2015) đã mang đến một cuộc cách mạng với hai từ khóa khai báo mới: let và const. Chúng giới thiệu một khái niệm phạm vi chặt chẽ hơn: Block Scope. Bất kỳ cặp dấu ngoặc nhọn {} nào, ví dụ như trong vòng lặp for hay câu lệnh if, đều tạo ra một Scope mới.
if (true) {
let blockMessage = "Đây là Block Scope!"; // Chỉ tồn tại trong khối if này
console.log(blockMessage);
}
// console.log(blockMessage); // Lỗi! ReferenceError: blockMessage is not defined
Đây là một cải tiến vượt bậc so với var. Hãy xem ví dụ kinh điển với vòng lặp:
// Dùng var (Function Scope)
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Luôn in ra 3, 3, 3
}, 100);
}
console.log("Giá trị cuối của i:", i); // In ra 3
// Dùng let (Block Scope)
for (let j = 0; j < 3; j++) {
setTimeout(function() {
console.log(j); // In ra 0, 1, 2 như mong đợi
}, 100);
}
// console.log("Giá trị cuối của j:", j); // Lỗi! j is not defined
Với let, mỗi vòng lặp tạo ra một biến j riêng biệt, giải quyết triệt để vấn đề mà các lập trình viên đã đau đầu với var trong nhiều năm.
Lexical Scope và Scope Chain hoạt động như thế nào?
Lexical Scope (hay Static Scope) là một khái niệm nghe có vẻ phức tạp nhưng thực ra rất logic. Nó có nghĩa là phạm vi của một hàm được quyết định tại thời điểm nó được viết ra, chứ không phải tại thời điểm nó được gọi.
Khi mã của bạn cần truy cập một biến, nó sẽ tìm kiếm trong Scope hiện tại của nó. Nếu không thấy, nó sẽ đi ra Scope cha ngay bên ngoài để tìm. Quá trình này cứ tiếp tục đi ra ngoài cho đến khi tìm thấy biến, hoặc cho đến khi chạm đến Global Scope. Chuỗi tìm kiếm từ trong ra ngoài này được gọi là Scope Chain.
const globalVar = "Tôi ở Global";
function outer() {
const outerVar = "Tôi ở Outer";
function inner() {
const innerVar = "Tôi ở Inner";
console.log(innerVar); // Tìm thấy ngay trong scope của inner
console.log(outerVar); // Không thấy, đi ra scope của outer -> Tìm thấy!
console.log(globalVar); // Không thấy, đi ra outer, vẫn không thấy, đi ra global -> Tìm thấy!
}
inner();
}
outer();
Đây chính là cách JavaScript quyết định biến nào được phép truy cập tại bất kỳ thời điểm nào.

Hoisting trong Javascript là gì?
Hoisting là một trong những hành vi đặc trưng của JavaScript khiến nhiều người mới học bối rối. Hiểu đơn giản, nó là cơ chế mặc định của JavaScript Engine, di chuyển phần khai báo (declaration) của các biến và hàm lên trên cùng của Scope chứa chúng (có thể là global, function hoặc block scope) trước khi mã được thực thi.
Quan trọng là, chỉ có phần khai báo được ‘kéo lên’, còn phần gán giá trị (assignment) vẫn ở nguyên vị trí cũ. Đây chính là mấu chốt gây ra nhiều hiểu lầm.
Giải thích cơ chế Hoisting
JavaScript Engine không thực thi code của bạn theo từng dòng từ trên xuống ngay lập tức. Thay vào đó, nó xử lý qua hai giai đoạn chính:
- Giai đoạn tạo (Creation Phase): Engine lướt qua toàn bộ code trong một Scope, tìm tất cả các khai báo biến (
var,let,const) và hàm (function). Nó cấp phát bộ nhớ cho chúng và ‘đặt chỗ’ trước. Đây chính là lúc Hoisting xảy ra. - Giai đoạn thực thi (Execution Phase): Engine bắt đầu chạy code của bạn từng dòng một. Khi gặp đến dòng gán giá trị, nó mới thực sự gán giá trị đó vào vùng nhớ đã được tạo ở giai đoạn trước.
Chính vì quá trình hai bước này mà bạn có thể sử dụng một biến hoặc hàm trước cả khi nó được viết trong code.
Hoisting với biến: Sự khác nhau giữa var, let và const
Đây là nơi sự khác biệt trở nên rõ rệt và cực kỳ quan trọng:
- Với
var: Khai báo được kéo lên đầu Scope và được khởi tạo với giá trịundefined. Đây là lý do tại saoconsole.log(myVar)không báo lỗi mà lại in raundefined.console.log(myVar); // Output: undefined var myVar = "Hello WiWeb!"; console.log(myVar); // Output: "Hello WiWeb!" // JavaScript hiểu đoạn code trên như sau: // var myVar; // console.log(myVar); // myVar = "Hello WiWeb!"; // console.log(myVar); - Với
letvàconst: Chúng cũng được hoist! Tuy nhiên, chúng không được khởi tạo với bất kỳ giá trị nào. Chúng rơi vào một trạng thái gọi là “Temporal Dead Zone”.
Temporal Dead Zone (TDZ) là gì?
Temporal Dead Zone (TDZ) là khoảng không gian từ đầu Block Scope cho đến dòng mà biến let hoặc const được khai báo thực sự. Nếu bạn cố gắng truy cập biến trong vùng TDZ này, JavaScript sẽ ném ra một lỗi ReferenceError.
// console.log(myName); // Lỗi! ReferenceError: Cannot access 'myName' before initialization
// Bắt đầu TDZ cho myName
let myName = "WiWeb Developer"; // Kết thúc TDZ cho myName
console.log(myName); // Output: "WiWeb Developer"
TDZ không phải là một lỗi của JavaScript. Ngược lại, nó là một cơ chế an toàn, giúp bạn tránh được các lỗi logic tiềm ẩn bằng cách buộc bạn phải luôn khai báo biến trước khi sử dụng. Nó làm cho code của bạn trở nên dễ đoán và chặt chẽ hơn.
Hoisting với hàm: Function Declaration vs Function Expression
Sự khác biệt khi hoisting hàm cũng là một điểm cần nắm vững:
- Function Declaration (Khai báo hàm): Toàn bộ hàm, bao gồm cả tên và phần thân hàm
{...}, đều được hoist. Điều này cho phép bạn gọi hàm ở bất cứ đâu trong Scope của nó, kể cả trước khi nó được định nghĩa.sayHello(); // Output: "Hello!" function sayHello() { console.log("Hello!"); } - Function Expression (Biểu thức hàm): Khi một hàm được gán cho một biến, nó tuân theo quy tắc hoisting của biến đó (
var,let, hoặcconst). Chỉ có phần khai báo biến được hoist, còn phần thân hàm thì không.// sayGoodbye(); // Lỗi! TypeError: sayGoodbye is not a function (vì sayGoodbye là undefined tại thời điểm này) var sayGoodbye = function() { console.log("Goodbye!"); }; sayGoodbye(); // Output: "Goodbye!"
Việc hiểu rõ sự khác biệt này giúp bạn quyết định nên sử dụng cú pháp nào cho phù hợp với cấu trúc và logic chương trình của mình.

Mối quan hệ mật thiết giữa Hoisting và Scope
Hoisting và Scope không phải là hai khái niệm độc lập. Chúng luôn song hành và tác động lẫn nhau. Cách một biến hoặc hàm được ‘hoist’ hoàn toàn phụ thuộc vào Scope mà nó được khai báo.
Hoisting luôn xảy ra bên trong một Scope cụ thể
Một lầm tưởng phổ biến là hoisting sẽ ‘kéo’ mọi thứ lên đầu file JavaScript. Thực tế không phải vậy. Hoisting chỉ diễn ra trong phạm vi của Scope hiện tại.
- Một biến khai báo với
varbên trong một hàm sẽ được hoist lên đầu hàm đó, chứ không phải Global Scope. - Một biến khai báo với
letbên trong một khối{}sẽ được hoist lên đầu khối đó, và TDZ của nó cũng chỉ tồn tại trong khối đó.
Điều này có nghĩa là, cơ chế ‘kéo lên’ tôn trọng tuyệt đối ranh giới mà Scope đã vạch ra.
Phân tích ví dụ thực tế: Thấy rõ sự tương tác
Hãy cùng xem một ví dụ lồng nhau để thấy rõ mối quan hệ này:
var x = 1; // Global Scope
function parentFunction() {
// Hoisting xảy ra trong scope của parentFunction
// Biến x bên dưới được hoist lên đây với giá trị undefined
console.log(x); // Output: undefined, không phải 1!
var x = 2; // Khai báo một biến x mới, che đi biến x toàn cục
console.log(x); // Output: 2
}
parentFunction();
console.log(x); // Output: 1
Tại sao console.log(x) đầu tiên lại là undefined?
- Khi JavaScript Engine vào
parentFunction, nó bắt đầu giai đoạn tạo cho scope của hàm này. - Nó thấy
var x = 2;và thực hiện hoisting: khai báovar xđược kéo lên đầuparentFunctionvà được khởi tạo làundefined. - Lúc này, trong scope của
parentFunctionđã tồn tại một biếnxcủa riêng nó. Biến này đã che (shadowing) đi biếnxở Global Scope. - Bắt đầu giai đoạn thực thi, dòng
console.log(x)được chạy. Nó tìm biếnxtrong scope hiện tại và thấy ngay biến vừa được hoist với giá trị làundefined. - Tiếp theo, dòng
x = 2mới được thực thi, gán giá trị cho biếnxcục bộ.
Ví dụ này cho thấy Hoisting và Scope hoạt động cùng nhau để tạo ra một môi trường thực thi có quy tắc rõ ràng, dù đôi khi có thể gây bất ngờ nếu chúng ta không nắm vững.

Tại sao hiểu rõ Hoisting và Scope lại quan trọng?
Việc dành thời gian để thực sự hiểu về Hoisting và Scope không chỉ là để vượt qua các bài kiểm tra lý thuyết. Nó mang lại những lợi ích vô cùng thiết thực trong công việc lập trình hàng ngày của bạn.
Tránh các lỗi logic và bug khó tìm nhất
Nhiều lỗi khó nhằn nhất trong JavaScript không phải là lỗi cú pháp, mà là lỗi logic xuất phát từ việc hiểu sai cách biến hoạt động. Ví dụ kinh điển về setTimeout trong vòng lặp for với var là minh chứng rõ nhất. Khi hiểu về block scope của let, bạn sẽ tự động tránh được cái bẫy đó. Hiểu về TDZ giúp bạn tránh việc vô tình sử dụng một biến trước khi nó có giá trị hợp lệ.
Viết mã JavaScript sạch hơn, dễ đoán và bảo trì
Khi bạn và đội nhóm của mình cùng chung một sự am hiểu sâu sắc về Scope và Hoisting, code được viết ra sẽ trở nên nhất quán và dễ đoán hơn rất nhiều. Bạn sẽ biết khi nào nên dùng const để đảm bảo tính bất biến, khi nào let là phù hợp, và gần như không bao giờ cần đến var. Điều này làm cho việc đọc code, review và bảo trì sau này trở nên đơn giản hơn đáng kể.
Chinh phục các câu hỏi phỏng vấn JavaScript kinh điển
Không có gì ngạc nhiên khi Hoisting và Scope là chủ đề yêu thích trong các cuộc phỏng vấn kỹ thuật cho vị trí lập trình viên JavaScript. Nhà tuyển dụng không chỉ muốn xem bạn có thể viết code hay không, mà còn muốn kiểm tra xem bạn có thực sự hiểu tại sao code lại chạy như vậy hay không. Việc giải thích rành mạch các ví dụ về TDZ, sự khác biệt giữa các loại function, hay hiện tượng shadowing sẽ cho thấy bạn có một nền tảng kiến thức vững chắc và tư duy lập trình sâu sắc.

Lời kết
Hành trình tìm hiểu về Hoisting và Scope có thể hơi phức tạp lúc đầu, nhưng chúng là những mảnh ghép không thể thiếu để bạn thực sự làm chủ JavaScript. Hãy nhớ những ý chính sau:
- Scope là quy tắc về tầm nhìn: Global (toàn cục), Function (cho
var), và Block (cholet/const). Luôn ưu tiên sử dụngletvàconstđể có phạm vi chặt chẽ và dễ kiểm soát nhất. - Hoisting là hành động ngầm của JavaScript: di chuyển các khai báo lên đầu scope.
varđược khởi tạo làundefined, trong khiletvàconstrơi vào Temporal Dead Zone (TDZ) cho đến khi dòng khai báo được thực thi. - Hai khái niệm này luôn đi đôi với nhau. Hoisting luôn tôn trọng ranh giới của Scope.
Hiểu rõ chúng không chỉ giúp bạn viết code tốt hơn mà còn giúp bạn tư duy như một JavaScript Engine thực thụ.
Phần nào về Hoisting và Scope khiến bạn ‘đau đầu’ nhất? Hãy chia sẻ với WiWeb trong phần bình luận bên dưới nhé!


05/02/2026
04/02/2026
03/02/2026
02/02/2026
21/01/2026
20/01/2026