40 questions
20 / 40 questions
var có function scope và bị hoisting với giá trị undefined. let và const có block scope và nằm trong TDZ (Temporal Dead Zone) cho tới dòng khai báo. const không cho gán lại binding, nhưng object/array mà nó trỏ tới vẫn có thể bị thay đổi nội dung.
const arr = [1];
arr.push(2); // ok, [1, 2]
arr = []; // TypeError
Thực tế nên ưu tiên const, dùng let khi cần gán lại, tránh var.
Hoisting là cơ chế JS đưa phần khai báo (không phải phần gán) lên đầu scope trước khi chạy. Khai báo function được hoist đầy đủ nên gọi trước khi định nghĩa vẫn chạy. var được hoist thành undefined; còn let/const được hoist nhưng nằm trong TDZ, truy cập sớm sẽ ném ReferenceError.
TDZ là khoảng từ đầu block đến dòng khai báo let/const, trong đó biến đã tồn tại nhưng chưa được khởi tạo. Truy cập biến trong vùng này sẽ ném ReferenceError thay vì trả về undefined như var.
console.log(x); // ReferenceError
let x = 1;
TDZ giúp bắt lỗi dùng biến quá sớm.
Closure là khi một hàm bên trong vẫn truy cập được các biến ở scope nơi nó được khai báo, kể cả sau khi hàm ngoài đã chạy xong. JavaScript dùng lexical scope nên scope đó được giữ sống chừng nào còn hàm tham chiếu tới nó.
function makeCounter() {
let count = 0;
return () => ++count;
}
const next = makeCounter();
next(); // 1
next(); // 2
Hay dùng để giữ state riêng tư, memoization, factory function.
=== so sánh không ép kiểu (strict): chỉ bằng khi cùng kiểu và cùng giá trị. == so sánh lỏng: ép kiểu trước khi so sánh (0 == '' là true). Thực tế nên luôn dùng === để tránh bug do ép kiểu ngầm.
Có đúng 8 giá trị falsy: false, 0, -0, 0n (BigInt), "", null, undefined và NaN. Mọi giá trị khác đều truthy, kể cả [], {} và "0".
if ([]) console.log('chạy'); // [] là truthy
NaN (Not-a-Number) là giá trị duy nhất không bằng chính nó theo chuẩn IEEE 754, nên mọi so sánh với nó đều false. Dùng Number.isNaN(x) (không ép kiểu) hoặc Object.is(x, NaN) để kiểm tra chính xác; tránh isNaN() global vì nó ép kiểu trước.
Number.isNaN(NaN); // true
isNaN('abc'); // true (ép kiểu, dễ nhầm)
Coercion là việc JS tự chuyển kiểu khi toán tử gặp kiểu không khớp. + với chuỗi thì nối chuỗi, còn - * thì ép sang số. Điều này gây ra kết quả bất ngờ.
1 + '2'; // '12'
'5' - 1; // 4
[] + {}; // '[object Object]'
Nên ép kiểu tường minh (Number(x), String(x)) và dùng ===.
typeof trả về chuỗi kiểu nguyên thủy ('number', 'string', 'function'...), hoạt động ở mức primitive. instanceof kiểm tra một object có nằm trên prototype chain của một constructor hay không.
typeof []; // 'object'
[] instanceof Array; // true
typeof null; // 'object' (bug lịch sử)
Với hàm thường, this phụ thuộc vào cách gọi (call-site): gọi như method thì this là object trước dấu chấm; gọi độc lập thì là undefined (strict mode) hoặc global object. Nó được xác định lúc runtime, không phải lúc khai báo.
const obj = { name: 'A', hi() { return this.name; } };
const fn = obj.hi;
fn(); // undefined - mất this
Arrow function không có this riêng; nó kế thừa this từ scope bao ngoài (lexical) tại nơi khai báo và không thể bị đổi bởi call/apply/bind. Vì vậy arrow rất tiện làm callback bên trong method, nhưng không nên dùng làm method của object hay constructor.
const obj = { n: 1, get() { return () => this.n; } };
obj.get()(); // 1
Cả ba đều đặt this thủ công. call(thisArg, a, b) gọi ngay với danh sách tham số; apply(thisArg, [a, b]) gọi ngay với mảng tham số; bind(thisArg, ...) không gọi mà trả về một hàm mới đã cố định this.
fn.call(obj, 1, 2);
fn.apply(obj, [1, 2]);
const bound = fn.bind(obj);
Mỗi object có một liên kết ẩn [[Prototype]] (truy cập qua Object.getPrototypeOf hoặc __proto__) trỏ tới object khác. Khi truy cập property không có trên chính object, JS đi ngược lên prototype chain để tìm. Đây là prototypal inheritance: object kế thừa từ object khác, không phải từ class như ngôn ngữ cổ điển.
const parent = { greet() { return 'hi'; } };
const child = Object.create(parent);
child.greet(); // 'hi'
JS chạy đơn luồng với một call stack. Khi stack rỗng, event loop lấy task từ hàng đợi để chạy. Có hai loại: macrotask (setTimeout, I/O, events) và microtask (Promise callback, queueMicrotask). Sau mỗi task, toàn bộ microtask queue được vét sạch trước khi tới macrotask kế tiếp.
console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
console.log(4);
// 1, 4, 3, 2
Microtask (Promise .then, queueMicrotask, MutationObserver) có ưu tiên cao hơn: được xử lý hết ngay sau khi task hiện tại kết thúc, trước khi render và trước macrotask. Macrotask (setTimeout, setInterval, sự kiện DOM) chạy mỗi vòng lặp một cái. Vì thế một Promise resolve luôn chạy trước setTimeout(fn, 0).
Promise là object đại diện cho kết quả của một tác vụ bất đồng bộ trong tương lai. Có ba trạng thái: pending, fulfilled (resolve) và rejected (reject). Khi đã settle thì trạng thái là bất biến. Dùng .then để lấy kết quả, .catch cho lỗi, .finally để dọn dẹp.
fetch(url).then(r => r.json()).catch(err => ...);
async/await là cú pháp giúp viết code bất đồng bộ trông như đồng bộ. Hàm async luôn trả về Promise; await tạm dừng hàm cho tới khi Promise settle rồi trả về giá trị đã resolve. Nó chỉ là lớp đường trên Promise, không tạo luồng mới.
async function load() {
const res = await fetch(url);
return res.json();
}
Dùng khối try/catch bao quanh await; nếu Promise reject, await sẽ ném lỗi và rơi vào catch. Với Promise chain thì dùng .catch. Lưu ý: một await bị reject mà không có try/catch sẽ tạo unhandled rejection.
try {
const data = await load();
} catch (err) {
console.error(err);
}
Callback hell là tình trạng lồng nhiều callback bất đồng bộ vào nhau tạo ra kim tự tháp khó đọc, khó xử lý lỗi. Khắc phục bằng cách dùng Promise để chain phẳng, hoặc async/await để viết tuần tự, và tách các callback thành hàm đặt tên riêng.
Promise.all chờ tất cả fulfilled, reject ngay khi có một cái fail. Promise.allSettled chờ tất cả settle và trả mảng kết quả {status, value/reason} (không bao giờ reject). Promise.race settle theo cái xong đầu tiên (dù resolve hay reject). Promise.any trả cái fulfilled đầu tiên, chỉ reject khi tất cả fail (AggregateError).