Luyện Phỏng Vấn IT — 2000+ Câu Hỏi Phỏng Vấn IT Có Đáp Án 2026

Danh mục

JavaScript iconJavaScript

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).

javascript
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 var sống trong toàn bộ hàm chứa nó.
  • Block scope: biến let/const chỉ 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 declaration
function sum(a, b) { return a + b }
- Có thể gọi trước chỗ khai báo (hoisting).

2) Function expression
const sum = function(a, b) { return a + b }
- Chỉ gọi được sau khi gán.

3) Arrow function
const 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.

javascript
function foo() { console.log(this); }
foo();                    // non-strict: Window (browser)
'use strict';
function bar() { console.log(this); }
bar();                    // strict: undefined

Kí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.

javascript
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); // 1

Giả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.

javascript
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.

javascript
// 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.
javascript
// 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.

javascript
// 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.

javascript
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.

javascript
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ụ:

javascript
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ụ:

javascript
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.
javascript
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.
javascript
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.

javascript
function makeCounter() {
  let count = 0;
  return () => ++count;
}
const c = makeCounter();
c(); // 1
c(); // 2

Hà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.

javascript
// 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.

javascript
// 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 arguments object ở 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ó arguments như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.

javascript
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.

javascript
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')); // false

Object.create(proto) tạo object mới với prototype là proto.

Object con dùng được method từ proto qua prototype chain.

javascript
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ạch

obj instanceof Foo kiểm tra: Foo.prototype có nằm trong prototype chain của obj không.

javascript
[] 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 Animal

Lư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.
javascript
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.

javascript
const target = { a: 1 };
const result = Object.assign(target, { b: 2 }); // mutates target
const spread = { ...target, c: 3 };             // new object

at(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:

javascript
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.

javascript
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.

javascript
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ừng

Hữ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).

javascript
// 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.

javascript
// 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 HTML

Debounce trì hoãn execution cho đến khi input ngừng, tránh gọi API quá nhiều lần.

js
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.

js
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.

js
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.

js
// 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.

js
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.

js
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.

js
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.

jsx
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...).

js
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.

js
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.

js
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ố đó.

js
// 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.

js
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.