Câu 1Điều gì xảy ra nếu bạn hiển thị unit ở dòng ngay sau if (true) { const unit = "yên"; }?
Scope và hoisting — Biến đọc được ở đâu và từ khi nào
Scope quyết định biến đọc được từ đâu, còn hoisting đăng ký khai báo trước khi code chạy. Kèm shadowing, TDZ của let và lý do bỏ var.
Đem một giá trị được tính bên trong cặp ngoặc nhọn ra dùng ở bên ngoài, chương trình sẽ dừng lại và báo lỗi. Đọc một giá trị cấu hình được khai báo ở dòng bên dưới cũng vậy. Cả hai lỗi đều xoay quanh một câu hỏi: một cái tên dùng được ở đâu và từ lúc nào.
Bài này nói về scope — phạm vi quyết định tên đã khai báo đọc được từ đâu — và hoisting — cơ chế đăng ký khai báo trước khi code chạy.
Biến chỉ dùng được trong dấu ngoặc nhọn — Block scope
Giả sử bạn muốn màn hình giỏ hàng hiển thị khách còn phải mua thêm bao nhiêu để được miễn phí vận chuyển. Số tiền còn thiếu có thể tính ngay trong if dùng để kiểm tra điều kiện miễn phí vận chuyển, nhưng nếu hiển thị giá trị đó ở ngoài dấu ngoặc nhọn thì chương trình dừng lại và báo lỗi ReferenceError.
Scope (phạm vi quyết định một tên đọc được từ đâu) được giới hạn bởi dấu ngoặc nhọn. Phạm vi của một cặp ngoặc nhọn gọi là block scope, và biến khai báo bằng const hoặc let chỉ đọc được bên trong cặp ngoặc nhọn gần nhất chứa dòng khai báo đó.
const cartTotal = 4800; // Tầng ngoài cùng, không nằm trong ngoặc nhọn nào
function checkShipping(total) {
const freeLine = 5000; // Chỉ trong ngoặc nhọn của checkShipping
if (total < freeLine) {
const shortage = freeLine - total; // Chỉ trong ngoặc nhọn của if
console.log(`Còn ${shortage} yên`); // Còn 200 yên
}
// Biến khai báo ở bên ngoài thì đọc được từ bên trong
console.log(cartTotal); // 4800
// console.log(shortage); // ReferenceError: shortage is not defined
}
checkShipping(cartTotal);
const cartTotal = 4800— đọc được từ bất kỳ đâu trong filecheckShipping(cartTotal)— gọi hàm từ tầng ngoài cùng
const freeLine = 5000— chỉ đọc được trong cặp ngoặc nàycartTotalở bên ngoài vẫn đọc được ở đâyconsole.log(shortage)— đã ra khỏi if nên không đọc được, gâyReferenceError
const shortage = 200— chỉ đọc được trong cặp ngoặc này- Đọc được cả
freeLinelẫncartTotal
if thì không đọc được nữa khi đã ra khỏi dấu ngoặc nhọn. Chỉ đọc được theo một chiều: từ trong ra ngoài.Tầng ngoài cùng, không nằm trong cặp ngoặc nhọn nào, là global scope, và biến ở đó đọc được từ bất kỳ đâu. Thân hàm cũng là một cặp ngoặc nhọn, nên bạn không thể đọc freeLine từ bên ngoài hàm. Phạm vi giới hạn bởi ngoặc nhọn của một hàm được gọi riêng là function scope.
Khai báo bên trong được ưu tiên — Shadowing
Khi thêm một hàm tính giá theo thuế suất giảm, có thể bạn sẽ khai báo trong hàm một biến tên taxRate, trùng tên với biến bên ngoài. Nếu không biết giá trị nào được dùng, bạn sẽ khó lần ra vì sao kết quả tính bị lệch.
Nếu scope bên trong khai báo lại một tên đã có ở scope bên ngoài, thì trong cặp ngoặc nhọn đó, khai báo bên trong sẽ được dùng. Hiện tượng này gọi là shadowing (khai báo bên trong che mất biến cùng tên ở bên ngoài), và nó xảy ra với cả ngoặc nhọn của if lẫn ngoặc nhọn của hàm.
const taxRate = 0.1; // Thuế suất đặt ở bên ngoài
function withTax(price) {
return Math.floor(price * (1 + taxRate));
}
function withReducedTax(price) {
const taxRate = 0.08; // Khai báo cùng tên bên trong hàm
return Math.floor(price * (1 + taxRate));
}
console.log(withTax(1000)); // 1100
console.log(withReducedTax(1000)); // 1080
console.log(taxRate); // 0.1 (biến bên ngoài không đổi)
taxRate, nhưng khai báo được tìm thấy sẽ khác nhau tùy vị trí đọc. Khai báo bên trong không ghi đè giá trị bên ngoài.JavaScript tìm một tên bắt đầu từ cặp ngoặc nhọn gần nhất chứa dòng đọc nó, rồi mới ra dần bên ngoài. Trong withReducedTax, việc tìm dừng lại ngay khi gặp 0.08, nên giá trị 0.1 bên ngoài không bao giờ được đọc. Cùng một tên mà mỗi hàm mang một giá trị khác nhau thì code rất khó theo dõi, nên khi tự viết, bạn hãy đặt tên riêng, chẳng hạn reducedTaxRate.
Quên const là gán vào biến bên ngoài
Nếu trong hàm bạn viết taxRate = 0.08; mà không có const, sẽ không có biến mới nào được tạo — đó là phép gán vào taxRate bên ngoài. Nếu biến bên ngoài là const, chương trình dừng lại và báo lỗi TypeError; còn nếu là let, giá trị bị ghi đè, và ngay cả lời gọi withTax(1000) sau đó cũng trả về 1080.
Chưa đọc được cho tới dòng khai báo — Hoisting và TDZ
Giả sử bạn muốn gom các giá trị cấu hình như tỷ lệ tích điểm hay phí vận chuyển xuống các dòng phía dưới đoạn code dùng chúng. Như đã thấy ở bài về hàm, khai báo function vẫn gọi được dù viết ở bên dưới, nhưng nếu đọc một giá trị cấu hình let hay const trước dòng khai báo của nó, chương trình sẽ dừng lại và báo lỗi ReferenceError.
Trước khi chạy code trong một scope, JavaScript đăng ký trước tên của mọi khai báo trong scope đó. Cơ chế này gọi là hoisting (khai báo được đăng ký trước khi code chạy). Khai báo function được đăng ký cả tên lẫn thân hàm cùng lúc, nên bạn gọi được nó từ những dòng phía trên khai báo. let và const thì chỉ đăng ký tên; giá trị chỉ được đưa vào khi dòng khai báo chạy.
// Thân hàm đọc pointRate vào lúc hàm được gọi
function calcPoint(amount) {
return Math.floor(amount * pointRate);
}
// Gọi ở đây thì dòng khai báo pointRate chưa được chạy tới
// console.log(calcPoint(3200));
// → ReferenceError: Cannot access 'pointRate' before initialization
let pointRate = 0.05; // Gom các giá trị cấu hình xuống dưới
console.log(calcPoint(3200)); // 160 (đọc được vì dòng khai báo đã chạy)
let chạy, giá trị vẫn chưa có, kể cả khi một hàm viết phía trên đọc nó. Đọc được hay không phụ thuộc vào thứ tự chạy, không phải vị trí bạn viết.Khoảng thời gian từ lúc vào scope cho tới khi dòng khai báo let chạy được gọi là TDZ (Temporal Dead Zone — giai đoạn một tên chưa đọc được cho tới khi dòng khai báo của nó chạy). Nếu thông báo lỗi là is not defined, tên đó hoàn toàn không có trong scope. Nếu là before initialization, tên đã tồn tại nhưng bạn đọc nó trước khi dòng khai báo chạy.
Được đăng ký sẵn với undefined — Vì sao không dùng var
Bạn sẽ gặp các biến khai báo bằng var trong những bài viết và thư viện cũ, và khi chép lại code chạy được, var có thể lẫn vào theo. Nhưng var khác const và let ở hai điểm: phạm vi mà biến có hiệu lực, và điều xảy ra khi bạn đọc nó trước dòng khai báo.
Khai báo var cũng được đăng ký trước, nhưng cùng lúc đó giá trị undefined được đưa vào. Vì vậy đọc nó ở dòng phía trên khai báo không gây lỗi — chương trình cứ thế chạy tiếp với undefined. Phạm vi hiệu lực của nó cũng không bị giới hạn bởi ngoặc nhọn, mà là toàn bộ function scope bao quanh.
function checkStock(orderCount) {
// Đọc trước dòng khai báo cũng không dừng, giá trị là undefined
console.log(stock); // undefined
var stock = 12;
console.log(stock); // 12
if (orderCount <= stock) {
var status = "Có thể giao hàng"; // Khai báo bằng var
let note = "Giao từ kho A"; // Khai báo bằng let
}
// var có hiệu lực trong toàn bộ ngoặc nhọn của hàm, nên đọc được bên ngoài if
console.log(status); // Có thể giao hàng
// console.log(note); // ReferenceError: note is not defined
}
checkStock(3);
if, giá trị var vẫn đọc được ở những dòng sau khi ngoặc nhọn kết thúc. Với let, chương trình báo lỗi ngay ở dòng đọc biến bên ngoài phạm vi.Mỗi tên var chỉ ứng với một biến duy nhất trong hàm, nên khai báo lại cùng tên bằng var bên trong if không tạo ra shadowing mà ghi đè lên chính biến đó. Bảng dưới đây cho biết, với từng cách khai báo, cái gì được lưu khi tên được đăng ký trước lúc chạy, và bạn nhận được gì khi đọc nó ở dòng phía trên khai báo.
| Cách khai báo | Được đăng ký trước khi chạy | Đọc ở dòng trên khai báo |
|---|---|---|
| var stock = 12 | Tên và undefined | Chạy tiếp với undefined |
| let stock = 12 | Chỉ có tên (chưa có giá trị) | Báo lỗi ReferenceError |
| const stock = 12 | Chỉ có tên (chưa có giá trị) | Báo lỗi ReferenceError |
Đổi var thành let có thể làm lộ ra lỗi
Khi thay var bằng let trong code cũ, những dòng đọc biến trước dòng khai báo, hoặc đọc ở bên ngoài if, sẽ báo lỗi ReferenceError. Đó là những dòng từ trước tới giờ vẫn âm thầm đọc undefined, hoặc đọc giá trị thuộc một cặp ngoặc nhọn khác. Để sửa, hãy chuyển dòng khai báo lên trên dòng đọc, và đặt nó ở cặp ngoặc nhọn bao quanh cả dòng đọc đó.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Ở gần đầu file, bạn định nghĩa hàm show trả về rate, rồi gọi nó ở một dòng bên dưới let rate = 0.1;. Hàm trả về gì?
Câu 3Nếu bạn viết console.log(count); ở một dòng phía trên var count = 3;, cái gì sẽ được hiển thị?