Skip to content
DevRecall
← Roadmap
03

JavaScript Core

drafted

Closures, this, prototypes, event loop, promises/async, modules, memory, number/date gotchas — kèm output-prediction puzzles và debug thực tế.

0/51 rated · mastery 0%

Bài học

11 bài · ~156 phút đọc. Học theo thứ tự, rồi làm câu hỏi.
  1. 01~14 phút· 4 câu

    Kiểu dữ liệu, coercion, == vs === và || vs ??

    Bảy kiểu primitive, object, luật ép kiểu ngầm (ToPrimitive, ToNumber), vì sao == khó đoán, khi nào ?? đúng hơn ||, và cách validate dữ liệu ở biên.

  2. 02~13 phút· 5 câuverify

    Number, BigInt, JSON, deep clone và Date

    IEEE 754 double và vì sao 0.1 + 0.2 lệch, biểu diễn tiền và ID 64-bit, giới hạn của JSON, structuredClone, và bug lệch ngày do timezone.

  3. 03~15 phút· 6 câuverify

    Scope, hoisting, TDZ và closure

    Lexical scope và environment record, var/let/const khác nhau thế nào, vì sao TDZ tồn tại, closure giữ gì, và stale closure trong React hooks.

  4. 04~13 phút· 4 câuverify

    this, arrow function và các cách gọi function

    Bốn luật binding của this theo thứ tự ưu tiên, arrow function khác regular function ở đâu, mất this khi truyền method làm callback, và tagged template literal.

  5. 05~13 phút· 4 câu

    Prototype chain, class, copy nông/sâu và prototype pollution

    Property lookup qua [[Prototype]], class thực chất biên dịch thành gì, vì sao spread chỉ là shallow copy, Object.freeze một tầng, và lỗ hổng prototype pollution khi merge JSON.

  6. 06~14 phút· 5 câuverify

    Event loop: call stack, macrotask, microtask và timer

    JavaScript single-threaded chạy thế nào, thứ tự sync → microtask → macrotask → render, vì sao promise có thể bỏ đói timer và UI, giới hạn của setTimeout, và xử lý việc nặng CPU.

  7. 07~14 phút· 5 câuverify

    Promise, async/await, combinator và giới hạn concurrency

    Promise state machine, chaining và error propagation, all/allSettled/race/any, bug forEach(async), return vs return await, promise pool tự viết, và chuẩn hoá code async trong một codebase lớn.

  8. 08~14 phút· 6 câuverify

    Error handling, unhandled rejection và cancellation

    try/catch bắt được gì và bỏ sót gì, unhandled rejection trong browser và Node, EventEmitter với listener async, thiết kế error class có cause, AbortController, và một httpClient có timeout/retry.

  9. 09~12 phút· 4 câuverify

    Iterator, generator, async iterator và xử lý dữ liệu lớn

    Iteration protocol, generator như hàm tạm dừng được, async generator cho API phân trang, for await với stream, backpressure, và cách xử lý file CSV triệu dòng không chặn event loop.

  10. 10~16 phút· 6 câuverify

    Module: ES Modules, CommonJS, circular import và migration

    Module được load, link và evaluate thế nào, live binding so với copy, circular dependency trong CJS và ESM, require(esm), dual package hazard, dynamic import và lỗi chunk, cùng conventions cho monorepo.

  11. 11~18 phút· 7 câuverify

    Memory, garbage collection, leak và hidden class trong V8

    Reachability và GC thế hệ của V8, những thứ giữ object sống (cache, listener, closure, timer), Map vs WeakMap vs WeakRef, tìm leak bằng heap snapshot ở Node và SPA, và vì sao shape của object ảnh hưởng tốc độ.

Tóm tắt & cheat sheet của track

TL;DR

  • JavaScript là ngôn ngữ single-threaded chạy trên một event loop; mọi thứ bất đồng bộ (timer, I/O, promise) đều là callback được xếp hàng chờ call stack rỗng.
  • Senior interview hiếm khi hỏi "closure là gì" — họ hỏi tại sao đoạn code này in ra thế này, leak ở đâu, promise nào bị nuốt lỗi.
  • Nắm chắc 5 trục: scope & closure, this binding, prototype chain, event loop (macro vs micro task), promise error propagation.

Khái niệm cốt lõi

Khái niệmMột câuVí dụ ngắn
ClosureFunction "nhớ" biến của scope nơi nó được tạoconst inc = counter(); inc(); inc() giữ count riêng
thisĐược quyết định lúc gọi, không phải lúc khai báo (trừ arrow function)obj.fn() → this = obj; const f = obj.fn; f() → undefined (strict)
PrototypeObject tra cứu property theo chuỗi __proto__class chỉ là syntax sugar trên prototype
MicrotaskPromise callbacks, queueMicrotask — chạy hết trước macrotask kế tiếpPromise.resolve().then(...) chạy trước setTimeout(..., 0)
TDZlet/const bị hoisted nhưng chưa khởi tạoTruy cập trước khai báo → ReferenceError
ESM live bindingimport là tham chiếu sống tới binding của module xuấtexport let n thay đổi → importer thấy giá trị mới
Generator / iteratorFunction tạm dừng được (yield), trả iterator lazyDuyệt API phân trang bằng for await...of
WeakMap / WeakRefGiữ reference không ngăn GC thu hồi key/targetMetadata gắn vào DOM node / request object
Number (IEEE 754 double)Mọi number là float 64-bit; số nguyên an toàn tới 2^53 - 10.1 + 0.2 !== 0.3; id 64-bit từ DB phải là string/BigInt

Cơ chế hoạt động

Event loop (browser & Node, mô hình chung)

Node.js có thêm process.nextTick queue (chạy trước promise microtasks) và các phase của libuv — xem track 05 Node.js Internals.

Promise lifecycle & error propagation

  • Mỗi .then/.catch trả về promise mới; lỗi "trôi" xuống tới handler gần nhất.
  • return trong .then quan trọng: quên return (hoặc quên await) = promise bị "floating", lỗi không tới caller.

Scope chain & closure

Closure giữ cả environment, không chỉ biến nó dùng (engine có tối ưu, nhưng đừng dựa vào) → callback sống lâu (timer, listener, cache) có thể giữ object lớn khỏi GC.

Trade-offs

Lựa chọnƯuNhượcDùng khi
async/await tuần tựDễ đọc, stack trace rõChậm nếu các bước độc lậpBước sau phụ thuộc bước trước
Promise.allSong song, fail-fastKhông giới hạn concurrency, không huỷ phần còn lạiVài request độc lập bắt buộc
Promise pool / p-limitSong song có giới hạnThêm code/dependencyHàng trăm+ task gọi DB/API
Callback / EventEmitterNhiều sự kiện theo thời gian, không cần promiseDễ leak listener, lỗi không propagateStream, socket, event bus
Async iterator (for await)Lazy, có backpressure tự nhiênTuần tự theo mặc địnhDuyệt phân trang, stream lớn
classRõ ràng, private #field, tooling tốtthis binding khi truyền method làm callbackDomain object có state
Closure / factoryPrivate tự nhiên, không lo thisMỗi instance một bản function (memory)Module nhỏ, hook, middleware
numberNhanh, nativeMất chính xác với tiền/số lớnĐếm, tính toán thông thường
BigInt / decimal lib / minor unitsChính xácChậm hơn, không trộn với numberTiền, id 64-bit

Pitfalls

  • forEach(async ...) không chờ gì cả; array.map(async) mà quên Promise.all.
  • Truyền obj.method làm callback → mất this; sửa bằng arrow field hoặc .bind.
  • const chỉ chặn gán lại, không làm object bất biến; Object.freeze chỉ freeze một tầng.
  • Spread {...obj} / [...arr] là shallow copy → mutate nested vẫn ảnh hưởng bản gốc (state React, Redux).
  • sort, reverse, splice mutate mảng gốc — dùng toSorted, toReversed, toSpliced (ES2023).
  • JSON.parse id 64-bit (9007199254740993) → mất chính xác; JSON.stringify BigInt → TypeError.
  • new Date('2026-09-29') là UTC nhưng new Date('2026-09-29T00:00') là local time → lệch ngày theo timezone.
  • Event listener / interval không cleanup → memory leak (SPA, Socket.IO client, long-running Node).
  • || thay cho ?? làm mất giá trị hợp lệ 0, '', false.

Cheat sheet

  • Promise.all fail-fast · allSettled không bao giờ reject · race = cái settle đầu tiên · any = cái fulfill đầu tiên (reject bằng AggregateError nếu tất cả fail).
  • async function luôn trả về Promise; throw trong nó = reject.
  • == coercion: null == undefined là true, nhưng null == 0 là false.
  • typeof null === "object" (bug lịch sử) · typeof function(){} === "function".
  • Arrow function: không có this, arguments, prototype; không dùng làm constructor.
  • var function-scoped + hoisted undefined · let/const block-scoped + TDZ · function declaration hoisted cả body.
  • ?? chỉ fallback khi null/undefined · ?. short-circuit khi null/undefined.
  • Number.MAX_SAFE_INTEGER = 2^53 - 1; so sánh float dùng epsilon; tiền dùng minor units (cents) hoặc decimal library.
  • structuredClone cho deep copy; spread chỉ shallow.
  • WeakMap key phải là object (hoặc non-registered symbol); không iterate được, không có size.
  • Microtask chạy hết trước macrotask kế tiếp → vòng lặp microtask vô hạn làm treo cả timer và render.
  • for...in duyệt key enumerable (kể cả kế thừa) · for...of duyệt giá trị qua iterator.

Interview Questions

Showing 51 / 51

function createRateLimiter(max: number) {
  let used = 0; // private, chỉ closure truy cập được
  return () => (used < max ? (++used, true) : false);
}
const allow = createRateLimiter(2);
allow(); allow(); allow(); // true, true, false
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
queueMicrotask(() => console.log('4'));
(async () => {
  console.log('5');
  await null;
  console.log('6');
})();
console.log('7');
async function saveAll(orders: Order[]) {
  const results = [];
  orders.forEach(async (o) => {
    try {
      results.push(await db.save(o));
    } catch (e) {
      logger.error(e);
    }
  });
  return results; // always []
}
const cache = new Map<string, User>();
export async function getUser(req: Request) {
  const key = `${req.headers.get('x-request-id')}:${req.userId}`;
  if (!cache.has(key)) cache.set(key, await db.users.find(req.userId));
  return cache.get(key)!;
}
console.log(typeof a, typeof b);
var a = 1;
let b = 2;
const cfg = { retries: 0, label: '' };
cfg.retries || 3; // 3  (bug)
cfg.retries ?? 3; // 0
const cart = { id: 'c1', items: [{ sku: 'A', qty: 1 }] };
const next = { ...cart };
next.items[0].qty = 5;
console.log(cart.items[0].qty); // ?
setTimeout(() => console.log('t1'), 0);
Promise.resolve().then(() => {
  console.log('p1');
  setTimeout(() => console.log('t2'), 0);
  Promise.resolve().then(() => console.log('p2'));
});
Promise.resolve().then(() => console.log('p3'));
console.log('s');
class Counter {
  count = 0;
  inc() { this.count++; }
}
const c = new Counter();
const { inc } = c;
try { inc(); } catch (e) { console.log((e as Error).constructor.name); }
setTimeout(c.inc, 0);
async function f() {
  try { return Promise.reject(new Error('x')); } catch { return 'caught'; }
}
async function g() {
  try { return await Promise.reject(new Error('x')); } catch { return 'caught'; }
}
// f() → ?   g() → ?
0.1 + 0.2;                                   // 0.30000000000000004
JSON.parse('{"id":9007199254740993}').id;    // 9007199254740992
new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }).format(125000);
async function* products(client: Api, signal?: AbortSignal) {
  let cursor: string | undefined;
  do {
    const page = await client.list({ cursor, limit: 100, signal });
    yield* page.items;
    cursor = page.nextCursor;
  } while (cursor);
}

for await (const p of products(api)) {
  await indexer.push(p);   // natural backpressure: next page fetched only when needed
  if (done()) break;       // break → generator's finally/return runs
}
async function getJson<T>(url: string, parent?: AbortSignal): Promise<T> {
  const signal = AbortSignal.any([AbortSignal.timeout(3000), ...(parent ? [parent] : [])]);
  const res = await fetch(url, { signal });
  if (!res.ok) throw new Error(`HTTP ${res.status}`, { cause: { url, status: res.status } });
  return res.json() as Promise<T>;
}
// counter.mjs                     // counter.cjs
export let count = 0;              let count = 0;
export function inc() { count++; } module.exports = { count, inc: () => { count++; } };

// main.mjs                        // main.cjs
import { count, inc } from './counter.mjs';
inc(); console.log(count);         const { count, inc } = require('./counter.cjs');
                                   inc(); console.log(count);
// API returns: { "deliveryDate": "2026-03-08" }
const d = new Date(order.deliveryDate);
label.textContent = `${d.getMonth() + 1}/${d.getDate()}`; // US users see 3/7
function sql(strings: TemplateStringsArray, ...values: unknown[]) {
  const text = strings.reduce((acc, s, i) => acc + s + (i < values.length ? `$${i + 1}` : ''), '');
  return { text, values };
}
const q = sql`SELECT * FROM orders WHERE tenant_id = ${tenantId} AND id = ${orderId}`;
// { text: 'SELECT * FROM orders WHERE tenant_id = $1 AND id = $2', values: [tenantId, orderId] }
class AppError extends Error {
  constructor(public code: string, message: string, options?: { cause?: unknown; status?: number }) {
    super(message, { cause: options?.cause });
    this.name = new.target.name;
  }
}
class NotFound extends AppError {}

try { await repo.get(id); }
catch (e) { throw new AppError('ORDER_LOAD_FAILED', 'Cannot load order', { cause: e }); }
function Elapsed() {
  const [sec, setSec] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setSec(sec + 1), 1000);
    return () => clearInterval(id);
  }, []);
  return <span>{sec}s</span>;
}
async function processAll(items: Item[]) {
  for (const it of items) {
    await transform(it); // transform is sync work wrapped in an async function
  }
}
setTimeout(() => console.log('fired'), 30 * 24 * 3600 * 1000);
// (node) TimeoutOverflowWarning: 2592000000 does not fit into a 32-bit signed integer.
// Timeout duration was set to 1.   → prints "fired" almost immediately
// config.ts
import { log } from './logger';
export const config = { level: process.env.LOG_LEVEL ?? 'info' };
log('config loaded');

// logger.ts
import { config } from './config';
const threshold = config.level; // top-level read, added in the refactor
export function log(msg: string) { if (threshold !== 'silent') console.log(msg); }
// entry: import './config'
import { EventEmitter } from 'node:events';
const bus = new EventEmitter();

bus.on('order.created', async (order) => {
  await inventory.reserve(order); // may reject
});

try {
  bus.emit('order.created', order);
} catch (e) {
  logger.error(e); // never runs
}
function merge(target: any, src: any) {
  for (const key in src) {
    if (typeof src[key] === 'object' && src[key] !== null) {
      target[key] ??= {};
      merge(target[key], src[key]);
    } else {
      target[key] = src[key];
    }
  }
  return target;
}
merge(settings, JSON.parse('{"__proto__": {"isAdmin": true}}'));
console.log(({} as any).isAdmin); // true
async function run() {
  try {
    JSON.parse('{bad');                             // (1)
    setTimeout(() => { throw new Error('t'); }, 0);  // (2)
    doAsyncThing();                                  // (3) returns a rejected promise, not awaited
    await Promise.reject(new Error('a'));            // (4)
  } catch (e) {
    console.log('caught', (e as Error).message);
  }
}