Mục lục
Xem mục lục
Scope là gì
Scope là phạm vi mà một biến có thể được truy cập trong code. Một biến chỉ tồn tại và dùng được trong đúng phạm vi nó được khai báo, cộng với các phạm vi con lồng bên trong đó.
JavaScript có ba loại scope chính: Global Scope, Function Scope, và Block Scope.
Global Scope
Global Scope là phạm vi truy cập được từ bất kỳ đâu trong chương trình. Biến khai báo ngoài mọi function và block thuộc phạm vi này:
const appName = "MyApp";
function show() {
console.log(appName); // truy cập được từ bên trong function
}
show();
Lạm dụng Global Scope dễ gây ô nhiễm global namespace: nhiều phần code không liên quan cùng đọc/ghi chung một biến toàn cục, dẫn tới xung đột tên hoặc thay đổi giá trị ngoài ý muốn. Nguyên tắc chung là khai báo biến ở scope hẹp nhất đủ dùng, tránh đẩy biến lên Global Scope khi không cần thiết.
Function Scope
Function Scope là phạm vi giới hạn trong một function, biến khai báo bên trong chỉ truy cập được từ function đó trở xuống.
Biến khai báo bằng var thuộc loại Function Scope, bị giới hạn trong function chứa nó, nhưng không bị chặn bởi block như if, for, while:
function example() {
if (true) {
var x = 10;
}
console.log(x); // 10, vẫn truy cập được dù khai báo trong block if
}
x được khai báo bên trong khối if, nhưng vì var chỉ tôn trọng ranh giới function, không tôn trọng ranh giới block, nên x vẫn truy cập được ở ngoài khối if, miễn còn trong cùng function.
Block Scope
Block Scope là phạm vi giới hạn trong một khối lệnh, tức cặp dấu ngoặc {} gần nhất, như thân của if, for, while, hoặc một khối {} đứng độc lập. Biến khai báo bên trong khối chỉ truy cập được trong chính khối đó, ra khỏi khối là hết phạm vi.
let và const thuộc loại Block Scope, giới hạn trong cặp {} gần nhất, đây là khác biệt cốt lõi so với var:
function example() {
if (true) {
let y = 10;
}
console.log(y); // ReferenceError: y is not defined
}
y chỉ tồn tại bên trong khối if, ra khỏi khối đó là hết phạm vi truy cập. Block scope áp dụng cho mọi cặp {}: if, for, while, hoặc một khối {} đứng độc lập không gắn với cấu trúc điều khiển nào.
Temporal Dead Zone (TDZ)
Temporal Dead Zone (TDZ) là khoảng thời gian từ đầu scope cho tới dòng khai báo biến, trong đó biến let/const đã tồn tại nhưng chưa thể truy cập.
var bị hoisting lên đầu function và được gán giá trị mặc định undefined, nên truy cập trước dòng khai báo không gây lỗi, chỉ trả về undefined:
console.log(a); // undefined, không lỗi
var a = 5;
let và const cũng bị hoisting, nhưng không được gán undefined ngay, nên truy cập trong TDZ gây lỗi:
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 5;
TDZ giúp phát hiện sớm lỗi dùng biến trước khi khai báo, thay vì để chương trình chạy tiếp với giá trị undefined một cách âm thầm như var.
Scope Chain
Khi engine cần tìm giá trị của một biến, nó tìm theo thứ tự từ scope trong cùng ra ngoài dần, gọi là Scope Chain, và dừng lại ngay khi tìm thấy biến ở scope gần nhất:
const outerVar = "outer";
function outer() {
const middleVar = "middle";
function inner() {
const innerVar = "inner";
console.log(innerVar); // tìm thấy ngay trong scope của inner
console.log(middleVar); // không có trong inner, tìm tiếp ra outer
console.log(outerVar); // không có trong outer, tìm tiếp ra Global Scope
}
inner();
}
outer();
Nếu đi hết Scope Chain tới Global Scope mà vẫn không tìm thấy biến, engine mới báo ReferenceError.
Ví dụ minh họa Scope Chain và Shadowing
Khi một biến ở scope trong trùng tên với biến ở scope ngoài, biến ở scope trong che khuất (shadowing) biến ở scope ngoài, trong phạm vi scope đó:
const value = "global";
function outer() {
const value = "outer";
function inner() {
const value = "inner";
console.log(value); // "inner"
}
inner();
console.log(value); // "outer"
}
outer();
console.log(value); // "global"
Mỗi scope có bản khai báo value riêng. Scope Chain luôn dừng lại ở bản khai báo gần nhất, nên inner không thể truy cập trực tiếp value của outer hay Global Scope một khi đã tự khai báo value của chính nó.
Vì sao nên ưu tiên let/const hơn var
Tổng hợp các vấn đề của var đã nêu ở trên:
- Không có block scope: biến rò rỉ ra ngoài khối
if/for, dễ gây nhầm lẫn và bug, như ví dụ ở phần Function Scope. - Không có TDZ thật sự: truy cập trước khai báo trả về
undefinedthay vì báo lỗi ngay, khiến lỗi logic khó phát hiện sớm. - Cho phép khai báo trùng tên trong cùng scope mà không báo lỗi, dễ vô tình ghi đè biến đã có:
var count = 1;
var count = 2; // không lỗi, count bị ghi đè âm thầm
let total = 1;
let total = 2; // SyntaxError: Identifier 'total' has already been declared
let và const khắc phục cả ba vấn đề trên: có block scope rõ ràng, có TDZ để báo lỗi sớm, và không cho khai báo trùng tên trong cùng scope. Khuyến nghị thực tế là mặc định dùng const, chỉ chuyển sang let khi biến cần được gán lại giá trị, và tránh dùng var trong code mới.
Tổng kết
- Scope là phạm vi một biến có thể được truy cập, gồm ba loại: Global, Function, Block.
varchỉ có Function Scope, không tôn trọng ranh giới block.let/constcó Block Scope, giới hạn trong cặp{}gần nhất.let/constcó Temporal Dead Zone, truy cập trước khai báo gây lỗi ngay, khác vớivartrả vềundefined.- Scope Chain tìm biến từ trong ra ngoài, dừng lại ở scope gần nhất tìm thấy biến đó; biến trùng tên ở scope trong sẽ shadowing biến ở scope ngoài.
- Nên ưu tiên
constmặc định, dùngletkhi cần gán lại, tránhvartrong code mới.