Luyện Phỏng Vấn IT — 2000+ Câu Hỏi Phỏng Vấn IT Có Đáp Án 2026
JavaScript
Hiểu nhanh:
- var: function scope, có thể khai báo lại, dùng trước khi khai báo sẽ ra undefined.
- let: block scope (if, for, {}), cho phép gán lại.
- const: block scope, không cho gán lại biến.
Ví dụ:const user = { name: "An" } vẫn sửa được user.name vì bạn đang sửa thuộc tính, không phải gán lại biến user.
Cho người mới: mặc định dùng const, khi cần đổi giá trị thì dùng let, tránh var.
JavaScript có 7 primitive types:
- string
- number
- boolean
- null
- undefined
- symbol
- bigint
Primitive được copy theo giá trị.
Ví dụ: let a = 5; let b = a; b = 10; thì a vẫn là 5.
Khác với object và array (copy theo tham chiếu).
Mẹo nhớ: typeof null trả về "object" là bug lịch sử.
==(loose): so sánh sau khi JavaScript tự ép kiểu.===(strict): so sánh cả giá trị và kiểu, không ép kiểu.
Ví dụ:
- 0 == "0" -> true
- 0 === "0" -> false
Best practice cho người mới: dùng === gần như mọi lúc.
Type coercion là việc JavaScript tự đổi kiểu khi chạy phép toán.
Ví dụ dễ gặp:
- "5" + 3 -> "53" (nối chuỗi)
- "5" - 3 -> 2 (ép sang number)
Vì vậy code có thể cho kết quả bất ngờ nếu không kiểm soát kiểu.
Cách an toàn: dùng === và ép kiểu tường minh bằng Number(), String(), Boolean().
Falsy là giá trị thành false khi đưa vào điều kiện if. Có đúng 8 giá trị:false, 0, -0, 0n, "", null, undefined, NaN.
Mọi giá trị khác là truthy, kể cả:
- "0"
- []
- {}
Mẹo: dùng Boolean(value) hoặc !!value để kiểm tra rõ ràng.
Hoisting nghĩa là JavaScript xử lý phần khai báo trước khi chạy code theo dòng.
Hiểu theo hành vi:
- var: được hoist và khởi tạo undefined.
- function declaration: hoist cả thân hàm, gọi trước vẫn được.
- let/const: cũng hoist nhưng chưa khởi tạo, truy cập sớm sẽ lỗi (TDZ).
console.log(a); // undefined (var hoisted)
console.log(b); // ReferenceError (let TDZ)
var a = 1;
let b = 2;
fn(); // hoạt động bình thường
function fn() { return "hoisted"; }Vì vậy var có thể trông như chạy được nhưng dễ gây bug khó debug.
- Function scope: biến
varsống trong toàn bộ hàm chứa nó. - Block scope: biến
let/constchỉ sống trong cặp{}gần nhất.
Ví dụ:for (var i = 0; i < 3; i++) {}
ra ngoài vẫn dùng được i (giá trị 3).
for (let j = 0; j < 3; j++) {}
ra ngoài thì j không tồn tại.
Vì vậy code hiện đại ưu tiên let/const.
Ba cách định nghĩa hàm trong JS, khác nhau về hoisting và this binding:
1) Function declarationfunction sum(a, b) { return a + b }
- Có thể gọi trước chỗ khai báo (hoisting).
2) Function expressionconst sum = function(a, b) { return a + b }
- Chỉ gọi được sau khi gán.
3) Arrow functionconst sum = (a, b) => a + b
- Ngắn gọn, không có this riêng, không dùng làm constructor.
Cho người mới: bắt đầu bằng declaration hoặc expression, dùng arrow nhiều cho callback.
Callback là hàm truyền vào hàm khác để được gọi sau.
Ví dụ:setTimeout(() => console.log("xong"), 1000)
Hàm arrow bên trong là callback.
Vì JavaScript chạy một luồng (single thread), callback giúp:
- không chặn app khi chờ API hoặc file
- làm việc bất đồng bộ (async)
Hiện nay ta thường viết async bằng Promise và async/await, nhưng callback vẫn là nền tảng cần hiểu.
Trong strict mode, this ở hàm thông thường là undefined thay vì global object (window/global).
Điều này giúp phát hiện lỗi khi vô tình gọi hàm như function thay vì method.
function foo() { console.log(this); }
foo(); // non-strict: Window (browser)
'use strict';
function bar() { console.log(this); }
bar(); // strict: undefinedKích hoạt bằng 'use strict'; ở đầu file hoặc hàm.
ES modules tự động bật strict mode.
Trong class, this trong constructor và methods trỏ đến instance được tạo.
Tuy nhiên, khi truyền method như callback, mất this binding.
class Counter {
count = 0;
// Class field arrow: auto-binds this
increment = () => { this.count++; };
}
const c = new Counter();
const { increment } = c; // destructure method
increment(); // OK — this vẫn là Counter instance
console.log(c.count); // 1Giải pháp: dùng arrow function trong class fields (tự bind), bind trong constructor, hoặc .bind() khi truyền callback.
__proto__ là instance link — nằm trên object instance. prototype là blueprint — nằm trên constructor function.
Khi dùng new Foo(), object mới có __proto__ trỏ tới Foo.prototype.
function Dog(name) { this.name = name; }
Dog.prototype.bark = function() { return 'Woof'; };
const d = new Dog('Rex');
d.__proto__ === Dog.prototype; // true
Object.getPrototypeOf(d) === Dog.prototype; // true (cách khuyến nghị)Trong code thực tế, dùng Object.getPrototypeOf(obj) thay vì truy cập __proto__ trực tiếp.
Destructuring cho phép giải nén giá trị từ array/object vào biến riêng lẻ.
Hỗ trợ default values, alias, và nested destructuring.
// Object destructuring:
const { name, age = 18, address: { city } } = user;
// Array destructuring (bỏ qua phần tử):
const [first, , third] = [1, 2, 3];
// Trong function parameter:
function greet({ name, role = 'user' }) {
return `Hi ${name} (${role})`;
}Spread mở rộng iterable thành từng phần tử (dùng trong function calls, array literals, object literals).
- Rest thu thập nhiều phần tử thành array (dùng trong function parameters).
- Cú pháp giống nhau nhưng ngữ cảnh ngược nhau.
// Spread: "mở ra"
const arr = [1, 2, 3];
console.log(Math.max(...arr)); // 3
const merged = [...arr1, ...arr2];
const copy = { ...obj, extra: true };
// Rest: "thu lại"
function sum(first, ...rest) { // rest là mảng
return first + rest.reduce((a, b) => a + b, 0);
}Template literals dùng backtick thay dấu nháy, hỗ trợ: interpolation ${expression}, multi-line strings, tagged templates.
Tagged templates là function nhận template strings và expressions làm arguments.
// Interpolation + multi-line:
const msg = `Hello ${name},
Welcome!`;
// Tagged template (styled-components pattern):
function css(strings, ...values) {
return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
}
const color = 'blue';
const style = css`color: ${color}; font-size: 16px;`;Class là cú pháp OOP rõ ràng hơn để tạo objects.
class Animal {
#name; // private field
constructor(name) {
this.#name = name;
}
speak() {
return this.#name + ' speaks';
}
static create(name) {
return new Animal(name);
}
}
const a = Animal.create('Cat');
a.speak(); // 'Cat speaks'Khác function constructor: class không thể gọi không có new (throw TypeError), methods không enumerable (không xuất hiện trong for...in), và phải khai báo trước khi dùng (không hoisted).
Bên dưới vẫn dùng prototype chain — class chỉ là syntactic sugar.
for...in lặp qua enumerable property names của object (cả own lẫn inherited từ prototype chain). for...of lặp qua values của iterable objects (Array, String, Map, Set, Generator). for...of thường dùng cho arrays, for...in cho objects.
Tránh for...in với arrays vì có thể lặp qua inherited properties từ prototype. Dùng Object.hasOwn(obj, key) trong vòng lặp for...in để chỉ xử lý own properties khi cần.
ES modules là hệ thống module chính thức của JavaScript. export để export bindings, import để import. Static analysis: imports/exports được phân tích tại parse time, không thể dynamic (dùng import() cho dynamic). Module có own scope, strict mode by default.
Live bindings là đặc điểm quan trọng của ES modules: imported binding là reference đến exported value, không phải copy. Nếu module xuất let count = 0 và tăng nó, người import sẽ thấy giá trị mới nhất — khác CommonJS require() vốn copy value tại thời điểm require. Điều này quan trọng khi import singleton objects hay shared state.
includes(str) kiểm tra có chứa substring không (thay indexOf !== -1). startsWith(str) / endsWith(str) kiểm tra đầu/cuối chuỗi. padStart(len, char) / padEnd(len, char) thêm ký tự vào đầu/cuối cho đủ độ dài — ví dụ '5'.padStart(3, '0') cho ra '005', hữu ích để format số thứ tự. trimStart() / trimEnd() xóa whitespace một phía. replaceAll(old, new) thay thế tất cả occurrences, khác replace() chỉ thay lần đầu.
Những methods này giúp code ngắn và dễ đọc hơn nhiều so với regex.
Promise là "lời hứa" cho kết quả của tác vụ async (ví dụ gọi API). Nó có 3 trạng thái:
- pending: đang chờ
- fulfilled: thành công (resolve)
- rejected: thất bại (reject)
Một khi đã fulfilled hoặc rejected thì không quay lại pending.
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve('done'), 1000);
// hoặc reject(new Error('failed')) nếu có lỗi
});
p.then(val => console.log(val)).catch(err => console.error(err));Promise giúp tránh callback hell và làm luồng async dễ đọc hơn.
.then(...): chạy khi Promise thành công..catch(...): chạy khi có lỗi ở Promise trước đó..finally(...): luôn chạy sau cùng (dù thành công hay lỗi).
Ví dụ:
fetch(url)
.then(r => r.json())
.then(data => console.log(data))
.catch(err => console.error(err))
.finally(() => setLoading(false))Mẹo cho người mới: dùng finally để dọn trạng thái UI như loading.
async/await là cú pháp viết Promise theo kiểu gần giống code đồng bộ, nên dễ đọc hơn chain .then().
Ví dụ:
async function loadUser() {
try {
const res = await fetch("/api/user")
const data = await res.json()
return data
} catch (err) {
console.error(err)
}
}Điểm cần nhớ:
- await chỉ dùng trong hàm async
- lỗi nên bắt bằng try/catch
- bản chất bên dưới vẫn là Promise.
DOM (Document Object Model) là biểu diễn dạng cây của HTML document, mỗi element là một node.
- JavaScript tương tác qua document API: querySelector, createElement, appendChild, setAttribute...
- DOM manipulation là synchronous, thay đổi DOM ngay lập tức reflect trên page.
const el = document.querySelector('.title');
el.textContent = 'Hello';
const btn = document.createElement('button');
btn.innerHTML = 'Click me';
document.body.appendChild(btn);getElementById trả về element đầu tiên có id, nhanh nhất. querySelector trả về element đầu tiên khớp CSS selector. querySelectorAll trả về NodeList tất cả elements khớp (static, không live). getElementsByClassName và getElementsByTagName trả về HTMLCollection (live). querySelectorAll phổ biến nhất vì linh hoạt.
preventDefault() ngăn hành vi mặc định của browser (submit form, follow link, check checkbox) nhưng không dừng propagation. stopPropagation() ngăn event bubble/capture lên/xuống DOM tree nhưng không ngăn default behavior.
Có thể dùng cả hai cùng lúc.
Cookies gửi kèm mọi HTTP request (có thể dùng HttpOnly để ngăn JS access, Secure cho HTTPS only). LocalStorage/sessionStorage chỉ accessible qua JS, không gửi với requests.
Cookies cho session, auth tokens. LocalStorage cho user preferences, cache. Tránh lưu sensitive data trong localStorage vì XSS.
Symbol là kiểu dữ liệu nguyên thủy tạo ra giá trị duy nhất (unique). Mỗi Symbol() tạo ra một giá trị không bao giờ bằng nhau dù có cùng description. Symbol thường dùng làm key cho object property để tránh xung đột tên, hoặc tạo các well-known symbols.
Well-known symbols quan trọng: Symbol.iterator (define iterable), Symbol.toPrimitive (customize type coercion), Symbol.hasInstance (customize instanceof). Symbol.for(key) tạo global shared symbol — Symbol.for('foo') === Symbol.for('foo') là true, khác Symbol('foo') !== Symbol('foo').
NaN (Not a Number) là giá trị đặc biệt xuất hiện khi phép tính số học thất bại: parseInt('abc'), 0/0, Math.sqrt(-1).
Theo chuẩn IEEE 754, NaN không bằng bất kỳ giá trị nào kể cả chính nó — NaN === NaN là false. Dùng Number.isNaN(val) để kiểm tra chính xác (không dùng isNaN() vì hàm global này ép kiểu trước: isNaN('hello') trả về true). Bẫy: typeof NaN trả về 'number' — NaN vẫn là kiểu number, chỉ là giá trị không hợp lệ.
TDZ là khoảng thời gian từ khi block bắt đầu đến khi biến let/const được khai báo. Trong TDZ, việc truy cập biến sẽ ném ReferenceError dù biến đã được hoisted.
Ví dụ: console.log(x); let x = 5; sẽ throw ReferenceError, trong khi var chỉ trả về undefined. TDZ giúp phát hiện lỗi dùng biến trước khi khai báo — điều var cho phép nhưng gây bug khó tìm. Khi phỏng vấn hỏi về TDZ, nhớ nêu rằng biến vẫn được hoisted nhưng chưa được khởi tạo.
Scope chain là cơ chế JavaScript tìm kiếm biến từ scope hiện tại ra ngoài đến global scope.
- Khi không tìm thấy biến trong scope hiện tại, JS tìm trong scope cha, rồi tiếp tục lên cho đến global.
- Nếu không tìm thấy ở global, ném ReferenceError.
const x = "global";
function outer() {
const x = "outer";
function inner() {
// không có x ở đây → tìm cha (outer) → tìm thấy "outer"
console.log(x); // "outer"
}
inner();
}Scope chain được tạo khi hàm được định nghĩa (lexical scope), không phải khi gọi.
Closure là hàm 'nhớ' được biến từ lexical scope bên ngoài, ngay cả sau khi hàm ngoài đã return.
function makeCounter() {
let count = 0;
return () => ++count;
}
const c = makeCounter();
c(); // 1
c(); // 2Hàm trả về vẫn truy cập count.
Ứng dụng thực tế: private variables (encapsulation), factory functions, event handlers giữ state, debounce/throttle, React hooks (useState bên trong dùng closure).
Khi dùng var trong vòng lặp for, tất cả callback chia sẻ cùng tham chiếu đến biến i — vì var là function scope, chỉ có một biến i duy nhất.
// Bug: in ra 3 3 3
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Fix 1: dùng let (block scope)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0 1 2
}
// Fix 2: IIFE capture giá trị
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 0);
})(i);
}Đây là câu hỏi phỏng vấn cổ điển về closure.
Function declaration được hoisted hoàn toàn (cả tên và thân hàm), có thể gọi trước khi khai báo.
Function expression chỉ hoisted phần khai báo biến (với var là undefined), không thể gọi trước khi gán.
// Function declaration — OK
fn(); // hoạt động
function fn() { return "hoisted"; }
// Function expression với var — TypeError
fnExpr(); // TypeError: fnExpr is not a function
var fnExpr = function() { return "not hoisted"; };
// const/let expression — ReferenceError
arrowFn(); // ReferenceError (TDZ)
const arrowFn = () => "also not hoisted";Higher-order function là hàm nhận hàm khác làm argument hoặc trả về hàm.
Ví dụ: map(), filter(), reduce() là HOF nhận callback. Hàm tạo middleware trong Express cũng là HOF. HOF là nền tảng của lập trình hàm (functional programming) trong JavaScript.
IIFE là hàm được định nghĩa và gọi ngay lập tức: (function() { const secret = 42; })() — biến secret không lọt ra global scope. Cú pháp arrow function: (() => { ... })().
Trước ES6, IIFE là cách duy nhất để tạo scope riêng vì chỉ có function scope. Dùng để: tránh ô nhiễm global scope, thực thi code khởi tạo một lần, tạo module pattern với private state. Ngày nay ít dùng hơn do có ES modules và block scope với let/const, nhưng vẫn hay gặp trong code cũ.
Pure function là hàm luôn trả về kết quả giống nhau với cùng input và không có side effects — không thay đổi state ngoài, không gọi API, không modify argument truyền vào.
Ví dụ: const add = (a, b) => a + b là pure, còn const push = (arr, val) => { arr.push(val); return arr; } là impure vì modify arr gốc. Pure functions dễ test (không cần mock), dễ debug, hỗ trợ memoization và là nền tảng của functional programming. Trong React, components và reducers nên là pure functions.
Rest parameters (...args) thu thập tất cả tham số còn lại thành Array thực sự: function sum(...nums) { return nums.reduce((a, b) => a + b, 0); }.
- Khác
argumentsobject ở 3 điểm: rest là Array thật (cómap,filter...) trong khi arguments chỉ là array-like; rest chỉ chứa các params chưa được đặt tên, còn arguments chứa tất cả; arrow functions không cóargumentsnhưng có rest params. - Bẫy: rest param phải là tham số cuối cùng —
(a, ...rest, b)là syntax error.
Function composition là tạo hàm mới bằng cách kết hợp nhiều hàm, output của hàm này là input của hàm kia: compose(f, g)(x) = f(g(x)).
Ví dụ: const process = compose(trim, toLowerCase, removeSpaces) tạo pipeline xử lý string. Implement bằng reduce: const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x). pipe là ngược chiều (trái sang phải, dễ đọc hơn). Hữu ích để tạo data transformation pipelines không cần biến trung gian, code theo functional style.
Callback hell là khi nhiều callbacks lồng nhau tạo code hình kim tự tháp, khó đọc và maintain — ví dụ: getUser(id, (user) => getPosts(user, (posts) => getComments(posts[0], (comments) => ...))).
Dễ xảy ra khi xử lý nhiều tác vụ async phụ thuộc nhau theo kiểu cũ. Giải pháp theo thứ tự ưu tiên: dùng async/await (rõ nhất), Promise chaining, hoặc named functions thay anonymous callbacks. Trong dự án thực tế, async/await là tiêu chuẩn hiện đại và nên dùng mặc định.
this được xác định theo 4 quy tắc ưu tiên giảm dần: 1) new binding (constructor), 2) explicit binding (call/apply/bind), 3) implicit binding (method call obj.fn()), 4) default binding (global/undefined trong strict mode).
Arrow functions không có this riêng, kế thừa từ lexical scope.
call(thisArg, arg1, arg2) gọi hàm ngay với this chỉ định, truyền args riêng lẻ. apply(thisArg, [arg1, arg2]) gọi ngay nhưng truyền args dạng array — mẹo nhớ: apply = array. bind(thisArg) trả về hàm mới với this cố định, không gọi ngay.
Ví dụ thực tế: const greet = function(greeting) { return greeting + ' ' + this.name; }; greet.call({name: 'An'}, 'Hello') trả về 'Hello An'. bind thường dùng khi truyền method như callback: btn.addEventListener('click', obj.method.bind(obj)).
Arrow function không có this riêng, kế thừa this từ lexical scope nơi nó được định nghĩa.
Khi dùng làm method, this không trỏ đến object chứa method.
const obj = {
name: 'Alice',
greet: () => this // arrow: this = global/undefined
};
console.log(obj.greet()); // undefined (hoặc Window)
const obj2 = {
name: 'Alice',
greet() { return this.name; } // regular method: OK
};
console.log(obj2.greet()); // 'Alice'Dùng function thông thường cho methods, arrow function cho callbacks bên trong method.
Khi gọi new MyFunc(), JS thực hiện 4 bước: 1) Tạo object rỗng mới, 2) Gán MyFunc.prototype làm prototype của object đó, 3) Chạy constructor với this là object mới, 4) Tự động return this — trừ khi constructor return tường minh một object khác.
Ví dụ: function Person(name) { this.name = name; } const p = new Person('An') tạo object {name: 'An'} với Person.prototype trong prototype chain. Bẫy: quên new sẽ khiến this trỏ vào global object (hoặc undefined ở strict mode), gây bug khó tìm.
Prototype chain là đường JavaScript đi tìm thuộc tính.
Nếu obj.name không có trên obj, JS tìm tiếp trên prototype... cho tới khi gặp null thì dừng.
const animal = { breathes: true };
const dog = Object.create(animal);
dog.bark = true;
console.log(dog.bark); // true — own property
console.log(dog.breathes); // true — inherited from animal
console.log(dog.__proto__ === animal); // true
// hasOwnProperty kiểm tra chỉ own property:
console.log(dog.hasOwnProperty('breathes')); // falseObject.create(proto) tạo object mới với prototype là proto.
Object con dùng được method từ proto qua prototype chain.
const parent = { greet() { return 'Hi!'; } };
const child = Object.create(parent);
child.greet(); // 'Hi!' — inherited
child.hasOwnProperty('greet'); // false — not own
// Object.create(null) — object không có prototype
const dict = Object.create(null);
'toString' in dict; // false — thật sự sạchobj instanceof Foo kiểm tra: Foo.prototype có nằm trong prototype chain của obj không.
[] instanceof Array; // true
[] instanceof Object; // true (Array.prototype.__proto__ === Object.prototype)
{} instanceof Array; // false
// Chain traversal:
class Animal {}
class Dog extends Animal {}
const d = new Dog();
d instanceof Dog; // true
d instanceof Animal; // true — vì Dog extends AnimalLưu ý: qua iframe/vm khác realm, instanceof có thể cho kết quả không như mong đợi.
Private fields khai báo với # prefix chỉ có thể truy cập trong class: #name.
Không thể truy cập từ bên ngoài kể cả subclass — truy cập field private từ bên ngoài ném TypeError tại runtime (không phải SyntaxError). SyntaxError chỉ xảy ra nếu sai cú pháp như obj.#field bên ngoài class. Đây là private thực sự (khác convention _ prefix), được implement ở engine level. Cũng có private methods (#method()) và static private.
Map lưu key-value với key là bất kỳ type nào, iterable, có size. WeakMap chỉ nhận objects làm key, không iterable, không có size.
Key trong WeakMap là weak reference: nếu object bị garbage collect, entry tự động bị xóa. WeakMap dùng để associate data với object mà không ngăn GC.
Set lưu collection của unique values bất kỳ type nào, iterable, có size. WeakSet chỉ chứa objects, không iterable, weak references (tránh memory leak).
Set dùng để loại bỏ duplicate, kiểm tra membership nhanh. WeakSet dùng để track objects mà không ngăn GC — ví dụ theo dõi các nodes đã visit trong graph traversal.
Optional chaining obj?.prop trả về undefined thay vì throw TypeError nếu obj là null/undefined — ví dụ user?.address?.city an toàn kể cả khi user hoặc address là null.
Nullish coalescing a ?? b trả về b chỉ khi a là null/undefined, khác || vốn trả về b với mọi falsy (0, '', false). Kết hợp cả hai: user?.profile?.age ?? 18 để lấy tuổi hoặc default 18. Bẫy: 0 || 5 trả về 5 nhưng 0 ?? 5 trả về 0 — dùng ?? khi 0 hoặc chuỗi rỗng là giá trị hợp lệ.
Proxy bọc object và chặn các operation (get, set, has, deleteProperty...) bằng handler traps.
- Reflect cung cấp methods tương ứng với các traps.
- Dùng để tạo reactive objects (Vue 3), validation, logging, lazy initialization, hay implement Observable.
const handler = {
get(target, key) {
console.log(`Getting ${key}`);
return Reflect.get(target, key);
}
};
const obj = new Proxy({ name: 'Alice' }, handler);
console.log(obj.name); // logs "Getting name" then "Alice"Cả hai đều shallow copy. Object.assign() copy vào object đích hiện có, trigger setters trên target. Spread tạo plain object mới, không trigger setters.
Object.assign() copy enumerable own properties bao gồm cả Symbol. Với nested objects, cả hai chỉ copy reference, không deep copy.
const target = { a: 1 };
const result = Object.assign(target, { b: 2 }); // mutates target
const spread = { ...target, c: 3 }; // new objectat(index) truy cập phần tử với hỗ trợ negative index: arr.at(-1) lấy phần tử cuối, arr.at(-2) lấy phần tử áp cuối. Thay cho arr[arr.length - 1] vốn dài và dễ nhầm. Hoạt động với Array, String và TypedArray — 'hello'.at(-1) trả về 'o'. Hỗ trợ trên tất cả modern browsers từ 2022.
Không thể dùng arr[-1] vì JS coi -1 là string key của object — đây là bẫy phỏng vấn phổ biến.
So sánh 4 combinators của Promise:
const p1 = fetch('/api/a');
const p2 = fetch('/api/b');
// all(): fail-fast nếu có 1 reject
const [a, b] = await Promise.all([p1, p2]);
// race(): lấy kết quả đầu tiên (fulfilled hoặc rejected)
const first = await Promise.race([p1, p2]);
// allSettled(): chờ tất cả, không reject
const results = await Promise.allSettled([p1, p2]);
// results[0] = { status: 'fulfilled', value: ... }
// results[1] = { status: 'rejected', reason: ... }
// any(): fulfilled đầu tiên, reject chỉ khi tất cả reject
const fastest = await Promise.any([p1, p2]);Microtasks: Promise callbacks (.then, .catch), queueMicrotask(), MutationObserver. Macrotasks (tasks): setTimeout, setInterval, setImmediate (Node.js), I/O callbacks.
Lưu ý UI rendering không phải macrotask — nó xảy ra giữa các tasks như một bước riêng của browser. Sau mỗi macrotask, tất cả microtasks trong queue được xử lý hết, sau đó browser có thể render. Thứ tự: sync code → microtasks → render → macrotask → ...
Dùng try/catch bọc await expressions.
Lỗi từ rejected Promise được catch như exception.
async function loadData() {
try {
const res = await fetch('/api/data');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error('Failed:', err.message);
return null;
} finally {
hideLoading(); // luôn chạy
}
}
// Hoặc chain .catch() bên ngoài
loadData().catch(err => console.error(err));
// Parallel: cần Promise.all để catch cả hai
const [a, b] = await Promise.all([fetchA(), fetchB()]);Không, setTimeout(fn, 0) vẫn đưa callback vào macrotask queue, chạy sau khi call stack rỗng và tất cả microtasks xử lý xong.
- Thực tế delay tối thiểu thường là 4ms trong browser (HTML spec).
- Dùng queueMicrotask() nếu muốn chạy sớm hơn (sau sync code nhưng trước I/O).
Race condition xảy ra khi nhiều async operations chạy đồng thời và kết quả phụ thuộc vào thứ tự hoàn thành.
Cách xử lý: dùng AbortController hủy requests cũ, track request ID và bỏ qua responses không mới nhất, dùng debounce, hoặc mutex/semaphore pattern.
Unhandled rejection xảy ra khi Promise bị rejected mà không có .catch() handler.
Trong Node.js v15+ sẽ terminate process theo mặc định (cấu hình qua --unhandled-rejections flag), trong browser hiện warning. Xử lý: luôn thêm .catch(), dùng process.on('unhandledRejection'), window.addEventListener('unhandledrejection'). Tốt nhất: luôn handle errors tại source.
Events propagate qua 3 phase: capturing (từ window xuống target), target, bubbling (từ target lên window).
- Mặc định addEventListener ở bubbling phase (useCapture = false).
- Capturing ít dùng nhưng xử lý trước bubbling. stopPropagation() dừng propagation, stopImmediatePropagation() dừng cả handlers cùng element.
Event delegation gắn một event listener lên parent thay vì nhiều listeners trên từng child.
- Dựa vào event bubbling, check event.target để xác định element nào được click.
- Hiệu quả hơn cho dynamic content (không cần re-attach), ít memory hơn, code sạch hơn.
Fetch API là interface Promise-based hiện đại cho HTTP requests. Khác XHR: Promise thay callbacks, cleaner API, hỗ trợ service workers, streams.
Lưu ý: Fetch không reject khi HTTP error (404, 500), chỉ reject với network failure - cần check response.ok. XHR hỗ trợ progress events và request cancellation dễ hơn.
Web Workers chạy JavaScript trong background thread riêng biệt, không block main thread. Giao tiếp qua postMessage. Không truy cập được DOM.
Dùng cho: heavy computation (image processing, encryption, data parsing), keeping UI responsive. SharedArrayBuffer cho phép share memory giữa workers.
IntersectionObserver theo dõi khi element đi vào/ra viewport hoặc ancestor element.
- Asynchronous, không block main thread, hiệu quả hơn scroll event listeners.
- Dùng cho: lazy loading images, infinite scroll, animations triggered by scroll, tracking ad visibility.
MutationObserver theo dõi thay đổi trên DOM tree (attribute changes, child additions/removals, text changes).
Asynchronous, callbacks chạy như microtask.
const observer = new MutationObserver(mutations => {
mutations.forEach(m => console.log(m.type, m.target));
});
observer.observe(document.body, { childList: true, subtree: true });
// observer.disconnect() để dừngHữu ích khi làm việc với third-party code thay đổi DOM, implement undo/redo, hay real-time updates.
ResizeObserver theo dõi thay đổi kích thước element.
CustomEvent cho phép tạo events tùy chỉnh với data đính kèm.
Dispatch với element.dispatchEvent(event).
// Tạo và dispatch
const event = new CustomEvent('userLoggedIn', {
detail: { userId: 42 },
bubbles: true,
});
document.dispatchEvent(event);
// Lắng nghe
document.addEventListener('userLoggedIn', e => {
console.log(e.detail.userId); // 42
});Bubbles và cancelable tùy chỉnh được.
Dùng cho component communication không dùng framework, pub/sub pattern trong vanilla JS.
requestAnimationFrame(callback) lên lịch callback trước repaint tiếp theo (~60fps).
Tự pause khi tab ẩn (tiết kiệm CPU/battery), sync với display refresh rate, smooth hơn setTimeout. Trả về ID để cancelAnimationFrame. Callback nhận timestamp để tính delta time.
innerHTML: get/set HTML markup (nguy cơ XSS nếu set user input). textContent: get/set text của tất cả nodes kể cả ẩn, không trigger reflow. innerText: chỉ lấy text hiển thị (aware of CSS), trigger reflow để tính style. textContent nhanh nhất và an toàn nhất cho text manipulation.
// XSS risk:
el.innerHTML = userInput; // NGUY HIỂM nếu userInput không được sanitize
// An toàn:
el.textContent = userInput; // luôn render as text, không execute HTMLDebounce trì hoãn execution cho đến khi input ngừng, tránh gọi API quá nhiều lần.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}Ứng dụng: search input (gõ xong 300ms mới gọi API), resize handler, auto-save.
Phỏng vấn thường hỏi viết tay để kiểm tra hiểu closure và timer.
Throttle giới hạn call rate xuống tối đa một lần mỗi interval — gọi ngay lần đầu rồi chặn.
function throttle(fn, limit) {
let inThrottle;
return (...args) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}Khác debounce: throttle gọi ngay lần đầu rồi chặn, debounce đợi hết delay mới gọi.
Dùng cho scroll, mousemove.
Deep clone sao chép đệ quy toàn bộ nested values mà không giữ reference gốc.
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
const clone = Array.isArray(obj) ? [] : {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) clone[key] = deepClone(obj[key]);
}
return clone;
}Lưu ý: không handle Date, RegExp, Map, Set, circular refs.
Production dùng structuredClone() hoặc lodash.
Flatten nested array bằng đệ quy reduce hoặc iterative spread.
// Recursive
function flatten(arr) {
return arr.reduce((acc, item) =>
acc.concat(Array.isArray(item) ? flatten(item) : item), []);
}
// Iterative
while (arr.some(Array.isArray)) arr = [].concat(...arr);Phỏng vấn thường hỏi để test hiểu recursion và Array methods.
Promise.all resolve khi tất cả promises resolve; reject ngay khi có 1 promise reject.
function promiseAll(promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) { resolve([]); return; }
const results = [];
let count = 0;
promises.forEach((p, i) => {
Promise.resolve(p)
.then(val => { results[i] = val; if (++count === promises.length) resolve(results); })
.catch(reject);
});
});
}Lưu ý: handle empty array (resolve ngay []).
Reject ngay khi 1 promise reject.
useLocalStorage persists state vào localStorage, tự đồng bộ khi value thay đổi.
function useLocalStorage(key, initial) {
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initial;
} catch { return initial; }
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}Test khả năng viết custom hooks với side effects.
useInfiniteScroll trigger callback khi sentinel element xuất hiện trong viewport.
function useInfiniteScroll(callback) {
const ref = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) callback();
});
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, [callback]);
return ref;
}Gán ref cho sentinel element cuối danh sách.
Mini Redux dùng Context + useReducer: store dùng chung cho toàn app mà không cần thư viện ngoài.
const StoreContext = createContext();
function StoreProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StoreContext.Provider value={{ state, dispatch }}>
{children}
</StoreContext.Provider>
);
}
const useStore = () => useContext(StoreContext);Phỏng vấn thường hỏi để test hiểu state management internals.
Exponential backoff retry một async call failing với delay tăng dần (1s, 2s, 4s...).
async function retry(fn, maxRetries = 3, delay = 1000) {
for (let i = 0; i < maxRetries; i++) {
try { return await fn(); }
catch (err) {
if (i === maxRetries - 1) throw err;
await new Promise(r => setTimeout(r, delay * 2 ** i));
}
}
}Dùng cho API calls không ổn định.
Delay tăng: 1s, 2s, 4s.
Deep equal so sánh đệ quy toàn bộ nested values — không dùng được === cho objects.
function deepEqual(a, b) {
if (a === b) return true;
if (typeof a !== 'object' || typeof b !== 'object' || !a || !b) return false;
const keysA = Object.keys(a), keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every(key => deepEqual(a[key], b[key]));
}Phỏng vấn thường hỏi để test recursion và edge cases (null, arrays).
Memoize caches kết quả function theo argument key, tránh tính toán lại những lần sau.
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}Lưu ý: JSON.stringify chậm cho args lớn.
Production dùng WeakMap cho object args, LRU cache giới hạn size.
FizzBuzz là bài toán kinh điển: lặp từ 1 đến n, in 'Fizz' nếu chia hết cho 3, 'Buzz' nếu chia hết cho 5, 'FizzBuzz' nếu chia hết cho cả hai, còn lại in số đó.
// Cách cơ bản — kiểm tra 15 trước
function fizzBuzz(n) {
for (let i = 1; i <= n; i++) {
if (i % 15 === 0) console.log('FizzBuzz');
else if (i % 3 === 0) console.log('Fizz');
else if (i % 5 === 0) console.log('Buzz');
else console.log(i);
}
}
// Cách nâng cao — nối chuỗi, không cần kiểm tra 15
function fizzBuzzV2(n) {
for (let i = 1; i <= n; i++) {
let result = '';
if (i % 3 === 0) result += 'Fizz';
if (i % 5 === 0) result += 'Buzz';
console.log(result || i);
}
}Trường hợp biên cần xử lý: n bằng 0, số âm, hoặc input không phải số.
LRU (Least Recently Used) Cache là cấu trúc dữ liệu loại bỏ phần tử ít được dùng gần đây nhất khi cache đầy.
Trong JavaScript, Map tự nhiên duy trì thứ tự chèn, nên có thể dùng Map thay cho Doubly Linked List để triển khai đơn giản hơn.
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.map = new Map(); // duy trì thứ tự insertion
}
get(key) {
if (!this.map.has(key)) return -1;
const val = this.map.get(key);
// di chuyển về cuối (mới dùng nhất)
this.map.delete(key);
this.map.set(key, val);
return val;
}
put(key, value) {
if (this.map.has(key)) this.map.delete(key);
this.map.set(key, value);
// xóa phần tử đầu (ít dùng nhất) nếu vượt capacity
if (this.map.size > this.capacity) {
this.map.delete(this.map.keys().next().value);
}
}
}LRU cache được ứng dụng rộng rãi: cache API response, memoization kết quả tính toán, và database query cache.
forEach dùng để duyệt mảng và thực hiện side effect — không trả về mảng mới nên không tốn thêm bộ nhớ cấp phát.
map tạo và trả về mảng mới cùng độ dài, tốn thêm bộ nhớ do phải cấp phát mảng — dùng khi cần biến đổi dữ liệu. reduce tích lũy các phần tử thành một giá trị duy nhất (số, object, hoặc mảng khác) — linh hoạt nhất nhưng khó đọc nếu logic phức tạp. Về hiệu năng: forEach nhanh nhất do không cấp phát mảng mới; map chậm hơn một chút. Quy tắc chọn: chỉ duyệt thì dùng forEach; biến đổi dữ liệu thì dùng map; tích lũy giá trị thì dùng reduce.
Closure giữ tham chiếu đến scope ngoài, ngăn garbage collector giải phóng bộ nhớ. Vấn đề xảy ra khi closure không cần thiết giữ tham chiếu đến đối tượng lớn hoặc DOM node đã bị xóa.
Ví dụ điển hình: event listener không được xóa giữ reference đến component đã unmount:
function setup() {
const largeData = new Array(1000000); // dữ liệu lớn
window.addEventListener("resize", () => {
console.log(largeData.length); // closure giữ largeData
});
// largeData không được GC vì event listener còn sống
}Phòng tránh: gọi removeEventListener khi không cần, null hóa tham chiếu, dùng WeakRef/WeakMap cho cache.
Trong browser, var khai báo ở global scope được thêm vào window object vì global execution context gắn với window. let và const ở global scope không thêm vào window, chúng nằm trong script scope riêng biệt. Đây là lý do tránh dùng var ở global scope.
globalThis là cross-environment reference hiện đại: hoạt động trong browser (window), Node.js (global), và Web Workers (self) — thay thế cho code viết typeof window !== "undefined" ? window : global.
Stale closure xảy ra khi closure nắm giữ giá trị cũ của biến thay vì giá trị mới nhất.
Thường gặp trong React hooks khi useEffect capture state cũ:
// Bug: stale closure
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // "count" luôn là 0 (stale)
}, 1000);
return () => clearInterval(id);
}, []); // [] → chạy 1 lần, capture count=0 mãi mãi
// Fix 1: functional update (không cần capture count)
setCount(c => c + 1);
// Fix 2: thêm dependency
}, [count]);
// Fix 3: useRef để giữ latest value
const countRef = useRef(count);
countRef.current = count; // luôn cập nhậtCurrying chuyển hàm nhiều argument thành chuỗi hàm mỗi hàm nhận một argument: f(a,b,c) thành f(a)(b)(c).
Ví dụ: const multiply = (a) => (b) => a * b; const double = multiply(2); double(5) // 10. Dùng cho partial application, compose functions, tạo hàm chuyên biệt.
Memoization là kỹ thuật cache kết quả của hàm dựa trên input để tránh tính toán lại.
Hiệu quả với pure functions có tính toán nặng hoặc đệ quy như Fibonacci.
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const expensiveFib = memoize(function fib(n) {
if (n <= 1) return n;
return expensiveFib(n - 1) + expensiveFib(n - 2);
});
expensiveFib(40); // nhanh — cache hit sau lần đầuLưu ý: JSON.stringify không phù hợp nếu args là object có circular reference.
Generator function dùng function* và yield, trả về iterator có thể pause và resume.
Mỗi lần gọi next() thực thi đến yield tiếp theo và trả về {value, done}.
function* counter() {
let n = 0;
while (true) {
yield n++;
}
}
const gen = counter();
gen.next(); // { value: 0, done: false }
gen.next(); // { value: 1, done: false }Dùng để tạo infinite sequences, lazy evaluation, hoặc kiểm soát luồng bất đồng bộ (trước khi có async/await).
Khác nhau ở phạm vi kiểm tra:
- Object.hasOwn(obj, key) / hasOwnProperty: chỉ kiểm tra key nằm trực tiếp trên object.
- key in obj: kiểm tra cả object và prototype chain.
Ví dụ: key được "thừa hưởng" từ prototype thì in là true, nhưng hasOwn là false.
Trong code mới, ưu tiên Object.hasOwn(obj, key).
Prototype pollution là lỗ hổng khi dữ liệu người dùng ghi vào Object.prototype (qua key như __proto__, constructor, prototype).
Hậu quả: nhiều object khác trong app bị ảnh hưởng ngoài ý muốn.
// Attack vector (lodash merge vulnerability - CVE-2018-3721):
const malicious = JSON.parse('{"__proto__":{"isAdmin":true}}');
Object.assign({}, malicious); // pollutes Object.prototype
console.log({}.isAdmin); // true — bất kỳ object nào cũng bị ảnh hưởng!Cách phòng tránh: chặn key nguy hiểm, không deep-merge input mù quáng, dùng Object.create(null) hoặc Map, cập nhật thư viện merge lên bản an toàn.
Iterable protocol yêu cầu object có method Symbol.iterator trả về iterator object (có next() trả về {value, done}).
Arrays, Strings, Maps, Sets implement sẵn. Custom iterables cho phép dùng for...of, spread, destructuring. Generator functions tự động tạo iterable.
class Range {
constructor(start, end) { this.start = start; this.end = end; }
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
return current <= end
? { value: current++, done: false }
: { value: undefined, done: true };
}
};
}
}
for (const n of new Range(1, 3)) console.log(n); // 1, 2, 3Logical assignment kết hợp phép logic với phép gán. &&= gán chỉ khi bên trái truthy: user.name &&= user.name.trim() — chỉ trim nếu name có giá trị. ||= gán khi bên trái falsy: config.timeout ||= 3000 để đặt default. ??= gán chỉ khi null/undefined (an toàn hơn ||= vì không ghi đè 0 hay ''): options.retries ??= 3.
Sự khác biệt quan trọng với ||= và ??=: nếu giá trị là 0 hoặc chuỗi rỗng, ||= sẽ ghi đè còn ??= thì không.
Array.from(arrayLike) và [...arrayLike] đều tạo array từ iterable.
- Array.from() mạnh hơn: nhận array-like không phải iterable (chỉ cần có
lengthvà indexed elements) và có tham sốmapFnđể transform ngay khi tạo. - Spread yêu cầu đúng iterable protocol.
// NodeList không phải true iterable trong mọi môi trường
Array.from(document.querySelectorAll('div')); // OK
// mapFn: tạo mảng 1-5 ngay lập tức
Array.from({ length: 5 }, (_, i) => i + 1); // [1, 2, 3, 4, 5]JS single-threaded nhưng xử lý async nhờ event loop.
Quy trình:
- Chạy hết sync code trên Call Stack.
- Xử lý hết Microtask queue.
- Browser có thể render.
- Lấy 1 Macrotask.
- Quay lại bước 2
console.log('1 sync');
setTimeout(() => console.log('4 macrotask'), 0);
Promise.resolve()
.then(() => console.log('2 microtask'))
.then(() => console.log('3 microtask 2'));
console.log('1b sync');
// Output: 1 sync → 1b sync → 2 microtask → 3 microtask 2 → 4 macrotaskMicrotasks luôn ưu tiên hơn macrotasks — Promise.resolve().then() chạy trước setTimeout(fn, 0).
Debounce: chỉ gọi hàm sau khi ngừng trigger một khoảng thời gian (dùng cho search input, resize).
- Throttle: gọi hàm tối đa một lần trong khoảng thời gian nhất định (dùng cho scroll, mousemove).
- Debounce = 'đợi nghỉ', throttle = 'giới hạn tần suất'.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
function throttle(fn, interval) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= interval) { last = now; fn(...args); }
};
}for await...of lặp qua async iterable — objects implement Symbol.asyncIterator trả về async iterator.
Mỗi iteration await giá trị tiếp theo. Dùng với streams, pagination APIs, WebSocket messages. Async generators (async function*) tạo async iterables dễ dàng.
async function* fetchPages(urls) {
for (const url of urls) {
const res = await fetch(url);
yield await res.json();
}
}
for await (const page of fetchPages(['url1', 'url2'])) {
console.log(page);
}AbortController tạo controller và signal.
Truyền signal vào fetch options, khi gọi controller.abort() request bị hủy với AbortError.
const controller = new AbortController();
const { signal } = controller;
fetch('/api/data', { signal })
.then(r => r.json())
.catch(err => {
if (err.name === 'AbortError') return; // bỏ qua cancel
throw err;
});
controller.abort(); // hủy requestQuan trọng khi component unmount (React useEffect cleanup), search với debounce, hay race conditions.
await tuần tự từng operation sẽ chậm hơn.
Chạy song song bằng cách tạo tất cả Promises trước rồi mới await.
// Sequential — chậm (mỗi request phải chờ cái trước)
const user = await fetchUser(id);
const posts = await fetchPosts(id); // chờ user xong mới gọi
// Parallel — nhanh hơn
const [user, posts] = await Promise.all([
fetchUser(id),
fetchPosts(id),
]);
// Tránh await trong loop (serial)
for (const id of ids) {
await fetchItem(id); // BAD: 1 by 1
}
// Dùng Promise.all cho parallel
const items = await Promise.all(ids.map(id => fetchItem(id)));requestAnimationFrame(cb) chạy callback trước lần repaint tiếp theo (~16ms), dùng cho animations để sync với display refresh. requestIdleCallback(cb) chạy khi browser idle giữa frames, dùng cho non-urgent tasks như analytics, prefetch. Cả hai quan trọng cho performance.
Lưu ý: requestIdleCallback đã có mặt trên Safari 18+ (2024). scheduler.yield() (Chrome 115+, Stage 2) là cách hiện đại hơn để ngắt long task mà không cần setTimeout(0).
Tasks chiếm hơn 50ms là long tasks, block main thread, gây jank (giật). Giải pháp: chia nhỏ tasks với setTimeout(0) hoặc scheduler.yield() (Chrome 115+, Stage 2 proposal), sử dụng Web Workers cho heavy computation, dùng requestIdleCallback cho background tasks. Chrome DevTools performance tab hiển thị long tasks.
scheduler.yield() là cách khuyến nghị hiện đại — cho phép browser render giữa các chunk mà không cần setTimeout(0).