`var` is function-scoped and hoisted to `undefined`. `let`/`const` are block-scoped with a temporal dead zone. `const` forbids reassignment but the value can still be mutated.
Category
JavaScript
Closures, async, prototypes, modules, and language internals.
117 questions
Function declarations are fully hoisted (callable before the line). `var` is hoisted and initialized to undefined. `let`/`const`/`class` are hoisted but uninitialized — accessing them before the declaration throws (Temporal Dead Zone).
Declarations are 'hoisted' to the top of their scope. `var` is hoisted and initialized to `undefined` (no TDZ). `let`/`const` are hoisted but in a Temporal Dead Zone (TDZ) until the declaration line — reading them throws ReferenceError. Function declarations are fully hoisted (callable before the line); function expressions and arrow functions follow the `var`/`let`/`const` rules of whatever declares them.
Hoisting: declarations are processed before code runs. `var` declarations hoist to function scope, initialized to `undefined`. Function declarations hoist *with* their value. `let`/`const` hoist but stay in the Temporal Dead Zone until the declaration line — accessing them throws ReferenceError. Class declarations are TDZ too.
`typeof` returns a primitive type string but is wrong for null ("object") and arrays. `instanceof` checks the prototype chain but fails across realms (iframes). The reliable cross-realm check is `Object.prototype.toString.call(x)` → "[object Array]" etc. For primitives use `typeof`; for arrays `Array.isArray`; for null `=== null`; for plain objects there's no perfect check.
Falsy: `false`, `0`, `-0`, `0n`, `''`, `null`, `undefined`, `NaN`. Everything else is truthy — including `'0'`, `'false'`, `[]`, `{}`. `==` does type coercion (avoid); `===` doesn't. `NaN !== NaN` (use `Number.isNaN`). `typeof null === 'object'`. Prefer `===` and explicit checks (`Number.isFinite`, `?? `, `Object.is`).
=== compares value + type; == coerces the operands following a fixed algorithm before comparing. Always use === except for one defensible idiom: `x == null` to check for null OR undefined.
=== is strict equality: same type AND same value, no conversion. == is loose equality: it coerces operands to a common type first, with surprising rules (null == undefined, '' == 0, [] == ![]). Rule: always use === except the one idiom `x == null`.
Explicit coercion is when you deliberately convert a type (Number(x), String(x), Boolean(x)). Implicit coercion is when JS auto-converts during operations (==, +, if conditions, template literals). Implicit is the source of many bugs — prefer explicit conversion and ===.
Primitives compared by value: `1 === 1` is true. Objects compared by reference: `{a:1} === {a:1}` is false (different references). `===` (strict equality) skips coercion; `==` coerces (`1 == "1"` is true). For deep object equality, use `Object.is` for special cases or a deepEqual utility. `Object.is(NaN, NaN)` is true; `NaN === NaN` is false.
|| falls back when the left side is FALSY (0, '', false, NaN, null, undefined). ?? falls back ONLY when the left side is null or undefined. Use ?? for defaults when 0, '', or false are valid values — || would wrongly discard them.
Arrow functions inherit `this`/`arguments` lexically, can't be used with `new`, and have no `prototype`. Regular functions get their own `this` based on the call site and can be constructors.
Top-level `this` in a script is the global object (`window` in browsers, `globalThis` in Node sloppy). In ES modules and strict mode it's `undefined`. Inside functions, it depends on how they're called; arrows inherit lexically.
Browser console (script): `this` at top level is `window` (or `globalThis`). Node CommonJS module: top-level `this` is `module.exports` (which is `{}` initially), NOT `global`. Node ESM: top-level `this` is `undefined`. Inside functions: same rules everywhere — depends on call site, strict mode, arrow vs regular.
No — arrow functions have no own 'this'; they capture it lexically from the enclosing scope, and call/apply/bind cannot override it. They also have no [[Construct]] internal method, so using 'new' with an arrow function throws a TypeError.
`call(thisArg, ...args)` invokes immediately with a given `this`. `apply(thisArg, argsArray)` is the same but with args as an array. `bind(thisArg, ...partials)` returns a new function with `this` permanently set. The polyfill closes over `thisArg` + partials and uses `apply` (or `call`) internally — plus a `new.target` check so the bound function still works as a constructor.
`this` is determined at call time by how the function is invoked. `call`/`apply` invoke immediately with a chosen `this`; `bind` returns a new function permanently bound to it.
When you detach a method (pass it as a callback), `this` is lost. Preserve it with: .bind(obj) to create a permanently-bound function, an arrow wrapper () => obj.method() that calls it as a method, or arrow class fields. bind is the canonical tool; arrow wrappers are common in callbacks/JSX.
`new F(args)` does four things: (1) create an empty object whose prototype is `F.prototype`, (2) call `F` with `this` bound to the new object, (3) if `F` returns an object, use that; otherwise use the new object, (4) return the result. Powers class instantiation and constructor functions. Arrow functions can't be `new`'d.
It depends entirely on HOW the method is called, not where it's defined. obj.print() → 'this' is obj. But pull the method off (const p = obj.print; p()) and 'this' is lost (undefined/global). Arrow methods capture lexical 'this', not obj. The lesson: 'this' is determined at call time.
The function form itself (declaration vs arrow) is essentially zero perf impact. What matters is identity stability across renders in React: a new arrow function created inside a component on every render creates a new reference, which busts React.memo on child components and adds churn for useEffect deps. The fix is useCallback, hoisting, or stable references — not changing function syntax. For non-React code, the difference is negligible.
Closure = function + the variables of its enclosing scope, kept alive because the function references them. Real uses: private state in factories/modules, memoization caches, partial application/currying, React hooks capturing renders' props, debounce/throttle holding their timer, event handlers needing per-instance config.
A closure is a function bundled with the variables in scope at the time it was created — it remembers and can mutate those variables long after the outer function has returned.
A closure is a function that retains access to variables from the scope it was defined in, even after that outer scope has returned. Lexical scoping means scope is determined by where code is written, not where it's called. Powers module patterns, factories, currying, React hooks' captured values, and event handler state.
A higher-order function takes a function as an argument, returns a function, or both. Examples: map/filter/reduce, setTimeout, event listeners, and function factories. They're the basis of composition, currying, and decorators in JS.
Currying transforms an n-arg function into a chain of unary calls: `f(a, b, c)` becomes `f(a)(b)(c)`. Implemented with closures. Useful for composition, point-free style, and reusable handlers. Distinct from partial application (which pre-fills some args once).
Currying: transform an n-arg function into a chain of unary functions — `f(a, b, c)` becomes `f(a)(b)(c)`. Partial application: pre-fix some args, returning a function expecting the rest — `f.bind(null, a)`. Both rely on closures. Useful for point-free style, event handlers per item, and composition. Overused, they hurt readability.
Closures: inner functions remember their enclosing scope. Currying turns `f(a,b,c)` into `f(a)(b)(c)` using nested closures. Combined patterns interviewers love: counter factories, once/memoize, partial application, debounce/throttle, infinitely-curryable functions like `sum(1)(2)(3)... .valueOf()`.
Four core JS concepts: closures (a function + its remembered lexical scope), the event loop (the scheduler that runs queued async callbacks on the single thread), hoisting (declarations are processed before execution — var/function hoisted, let/const in the temporal dead zone), and currying (transforming f(a,b,c) into f(a)(b)(c) via closures).
Every object has an internal `[[Prototype]]` link to another object. Property lookup walks that chain. `Object.create`, `class`, and constructor functions all set up the same chain.
Classical (Java/C++): classes are blueprints; objects are instances. Inheritance is declarative + compile-time. Prototypal (JS): objects inherit from other **objects** via the [[Prototype]] chain — runtime-flexible. ES6 `class` is syntactic sugar over prototypes. In practice both look similar; the JS difference is that you can compose objects dynamically (`Object.create`, mixins, delegation).
Every object has a hidden [[Prototype]] pointer. Property lookup walks this chain. SDKs that need to extend native types do so safely by **subclassing or adding namespaced static helpers**, not patching `Array.prototype` etc. — that pollutes globals and can collide with merchant code. The safe API gives consumers an SDK object whose own prototype chain is private.
Use `class` for encapsulated kinds (component, model, service). Fields for state, methods on prototype, private fields with `#`, `static` for class-level helpers, `extends` + `super` for inheritance. Favor composition + small classes; avoid deep hierarchies. For pure-data shapes use plain objects + TypeScript types, not classes.
Destructuring pattern-matches a value's shape: arrays by index, objects by key. Supports defaults (`= 0`), renaming (`{a: b}`), rest (`...rest`), nested patterns, and works in parameters. Under the hood, it desugars to indexed/property access — array destructuring uses the iterator protocol.
Spread expands an iterable/object into elements; rest collects the remainder into an array/object. Destructuring binds positions/keys to variables with optional defaults and renames. All shallow — nested values still share references.
Same `...` syntax, opposite jobs. Spread EXPANDS an iterable/object into individual elements (copying arrays/objects, passing args). Rest COLLECTS multiple elements into one array (variadic params, destructuring). Both do shallow copies — nested objects are still shared.
Shallow copy duplicates the top level; nested objects are still shared references. Deep copy recursively duplicates every level. Use `structuredClone` for a correct, fast deep copy.
Shallow copy duplicates the top-level container but reuses inner references. Structural sharing reuses every untouched subtree across versions — the basis of efficient immutable data.
Object: string/symbol keys, prototype chain (collisions like `__proto__`), no insertion order guarantee for integer-like keys, JSON-friendly. Map: any key (including objects, NaN, null), preserves insertion order, has `.size`, no prototype chain. Use Map when keys are non-strings, you need size, or iteration order matters; object for JSON-shaped data.
Object: keys are strings/symbols, key order is mostly insertion (with integer-key quirks), prototype chain pollution. Map: any key type (objects, NaN), guaranteed insertion order, .size in O(1), better for frequent add/remove. Use Map for dictionaries; Object for shaped records.
Map/Set variants that hold their keys/values weakly — if nothing else references the object, it can be garbage-collected and the entry disappears. Keys must be objects; not iterable; no size. Use for: per-object private data, object-keyed caches that self-clean, and marking objects without leaking memory.
WeakMap/WeakSet hold their keys/values WEAKLY — if nothing else references a key, it can be garbage-collected, and the entry vanishes. Keys must be objects. They're not iterable and have no size. Use cases: private data per object, caching/memoization keyed by object, and tracking objects without leaking memory.
Avoid functional iteration when: you need early exit (use `for` / `for..of` / `find` / `some`), you're chaining many passes over a large array (multiple traversals + allocations — use one for loop), the side effect is the point (`forEach`, but `for..of` reads better), or readability suffers from a tangled `reduce`. Idiomatic for transforms; not a universal hammer.
Modern engines use **TimSort** (V8 since ~2018) — stable, O(n log n). Default comparator is **string** (lexicographic): `[1, null, 5, 2, undefined].sort()` → `[1, 2, 5, null, undefined]` because `undefined` is always moved to the end and `null` stringifies to `"null"`. Always pass a comparator for numbers.
Objects representing the eventual outcome of an async operation. Three states: pending → fulfilled or rejected, settled once and then immutable. Read via `.then` / `.catch` / `.finally` or await. Static aggregators: `Promise.all`, `allSettled`, `race`, `any`. Replaced callbacks with composable async.
A Promise is an object representing the eventual result of an async operation. States: pending → fulfilled or rejected (once, then immutable). Create with `new Promise((resolve, reject) => …)`. Use for: fetch, timers, file I/O, any async API — and chain with `.then` / `.catch` or await. Promises moved JS off callback hell.
Write a Promise-returning function that resolves with a value. Most common form: `function task() { return new Promise(res => setTimeout(() => res(value), ms)); }`. Or short: `return Promise.resolve(value)`. Real-world: wrap a non-Promise API (geolocation, FileReader, setTimeout) so it composes with async/await.
A Promise wraps an async operation: `new Promise((resolve, reject) => …)`. `async/await` is syntactic sugar — `async` makes a function return a Promise; `await` pauses until the awaited Promise settles. Same semantics, cleaner control flow.
`async` makes a function return a Promise; `await` pauses the function until the awaited Promise settles. Same semantics as `.then` chains, but reads sequentially. Errors become rejected Promises; catch with try/catch or .catch at the call site. Independent awaits should be `Promise.all`'d, not sequential.
async/await is syntax sugar over Promises — same machinery. `await` pauses the async function and schedules its continuation as a microtask when the awaited Promise settles. So `await x` ≈ `.then(continuation)`. The event loop treats both identically: continuations are microtasks, drained fully before the next macrotask.
async/await is syntax sugar over promises and generators. `await` pauses the function, yields to the microtask queue, and resumes when the promise settles. Same semantics, dramatically better readability and error handling.
JS is single-threaded; async work uses the event loop. Mechanisms: Web APIs / Node libuv (timers, I/O, network) → task queues (macrotasks for setTimeout, I/O; microtasks for Promises, queueMicrotask). Event loop picks one task → drains microtasks → renders → repeat. Workers give true parallelism. AbortController for cancellation.
Duplicate-style question on the event loop and async execution: JS hands async work to Web APIs, which queue callbacks (microtask for Promises, macrotask for timers/events) on completion; the event loop drains all microtasks then runs one macrotask whenever the call stack is empty.
Default to async/await for readability. Use Promise.all for parallel independent work, Promise.allSettled when you want every result regardless of failure, Promise.race / any for first-to-finish. Always wrap awaits in try/catch (or .catch on the call site). Pass an AbortSignal for cancellation. Never await sequentially when work is independent.
Wrap awaits in try/catch or attach `.catch` at the call site. Unhandled rejections in browsers fire `unhandledrejection` event and console warning; in Node, eventually crash (since v15). Distinguish error kinds (network, 4xx, 5xx, AbortError). Use error boundaries (React) at UI level. Don't swallow errors with empty catch — escalate or convert to domain errors.
Node.js is a JS runtime built on V8 that runs JS outside the browser, using libuv for async I/O. Its event loop has distinct phases — timers, pending callbacks, poll, check (setImmediate), close — and between every phase it drains microtasks (Promises) and process.nextTick (which runs even before Promises). Same single-threaded model, different phase structure than the browser.
JavaScript is single-threaded with one call stack. Async work (timers, fetch, events) is handed to Web APIs / the host. When it completes, a callback is queued. The event loop runs: when the call stack is empty, it drains ALL microtasks (Promises, queueMicrotask), then takes ONE macrotask (timer, I/O, event) and repeats — rendering can happen between macrotasks.
The full model: the call stack runs synchronous code; Web APIs handle async work off-thread; completed callbacks land in either the callback (macrotask) queue or the microtask queue; the event loop, when the stack is empty, drains all microtasks then runs one macrotask, repeating. Each component has a distinct role.
An execution context is the environment a piece of code runs in — it has a variable environment, scope chain, and `this`. The global context is created first; each function call pushes a new context on the call stack. Async behavior layers on top: Web APIs run async work off-thread and queue callbacks; the event loop pushes them back as new contexts when the stack is empty.
Microtasks (Promise reactions, queueMicrotask, MutationObserver) are drained ENTIRELY after each task and before rendering. Macrotasks (setTimeout, setInterval, DOM events, I/O) run ONE per loop iteration. So all pending Promise callbacks run before the next setTimeout — and an unbounded microtask chain can starve macrotasks and block paint.
The event loop drains the entire microtask queue after every macrotask, then renders. Misunderstanding this order causes subtle async bugs.
Execution order: (1) all synchronous code on the call stack, (2) drain the entire microtask queue, (3) optional render, (4) one macrotask, then back to step 2. So output order is: sync → microtasks → (render) → one macrotask → microtasks again, etc. Work through console.log/setTimeout/Promise puzzles with this rule.
Synchronous code blocks the main thread until it finishes — long loops, big JSON.parse, sync XHR, alert/confirm/prompt, document.write, deep recursion. While blocked, no events, no rendering, no timers. Mitigate: break into chunks (yield to microtask/macrotask), use Web Workers for CPU, async APIs only, and avoid the listed blocking calls in production.
Classic 'what's the output' problems hinge on: microtask queue order (Promises before setTimeout), synchronous code first, async function continuations are microtasks, await suspends even when value is already resolved, Promise.resolve().then(...) doesn't immediately invoke. Trace stack → micro → macro and you'll predict any ordering.
A Promise is a placeholder for a future value — pending → fulfilled or rejected, settled once. For multiple async ops: `Promise.all` (fail-fast parallel), `Promise.allSettled` (parallel, never rejects, returns per-result status), `Promise.race` (first to settle), `Promise.any` (first to fulfill, ignores rejections). Use `for await…of` or a worker pool when you need bounded concurrency.
Promise.all (all succeed or reject on first failure), Promise.allSettled (wait for all, get every outcome), Promise.race (first to settle, success or failure), Promise.any (first to succeed). Plus sequential await-in-loop vs parallel. Choose by: need all? tolerate failures? need just one?
all = fail-fast aggregation. allSettled = collect every outcome. race = first to settle (resolve or reject). any = first to *resolve*, ignores rejections until all fail.
`Promise.all` rejects as soon as any input rejects — short-circuits. `Promise.allSettled` always resolves once every input has settled, with an array of `{status, value|reason}` objects. Use `allSettled` for independent best-effort work (dashboard cards, batch operations, analytics) where partial success is meaningful.
Fire all with `Promise.all` if independent and you need all results; `Promise.allSettled` if you want every result regardless of failure; `p-limit` or a small async-pool for many requests (avoid hammering the server); chain awaits only when one depends on another's output. Add AbortController for cancel-on-unmount. Disable the button while in flight.
Choose by semantics: `Promise.all` rejects on first failure — good for atomic ops. `Promise.allSettled` collects per-item outcomes — good for independent work. Retry transient failures with exponential backoff. Use AbortController to cancel siblings if one critical request fails. Surface partial-success states explicitly in UI.
fetch has no built-in timeout. Combine AbortController + setTimeout: create a controller, schedule abort() after N ms, pass signal to fetch, and clear the timer in finally. Always clear the timer to avoid aborting a now-completed request. Wrap into a reusable fetchWithTimeout helper. On modern platforms, AbortSignal.timeout(ms) is a one-liner replacement.
ESM is statically analyzable, async, the standard. CJS is dynamic, sync, Node-historical. Modern code is ESM; CJS lives on for legacy compatibility. Mixing them is the source of many Node interop bugs.
import() is a function-like expression that loads a module on demand and returns a Promise. It enables code splitting, lazy loading, conditional/feature-flagged loading, and reduced initial bundle size — the foundation of React.lazy and route-based splitting.
`dependencies` are needed at runtime; `devDependencies` are only needed for building, testing, or linting. Consumers of your package install dependencies but skip devDependencies.
Both are package managers for the npm registry. Yarn originally fixed npm's speed, lockfile, and determinism gaps; npm has since caught up (package-lock, npm ci, workspaces). Today differences are small — Yarn Berry adds PnP/zero-installs; pnpm is the notable alternative with a content-addressed store.
A closure over a timer id: each call clears the pending timeout and schedules a new one, so the function runs only after calls stop for the wait period. Preserve this/arguments with apply, and add a cancel method for cleanup.
Debounce: delay invoking until N ms have passed since the last call. Implementation captures a timer in a closure; each call clears the prior timer and schedules a new one. Variants: leading edge (call immediately, then ignore), trailing (default), `flush` / `cancel` methods, AbortSignal support.
Return a function that resets a timer on every call and only invokes fn after `wait` ms of silence. Forward `this` and arguments, expose cancel/flush, and optionally support a leading-edge call.
Debounce delays running a function until it stops being called for a set wait time — each call resets a timer. Implement with a closure over a timer id: clear the previous timeout and set a new one. Used for search-as-you-type, autosave, and resize handlers.
Throttle = call the function at most once per N ms regardless of how often the trigger fires. Two flavors: leading (fire immediately, then ignore until cooldown ends) and trailing (fire on the last call within the window). Track lastCall timestamp; compare to now; schedule a trailing call if needed.
Debounce delays the call until activity stops; throttle caps how often the call can fire. Both control noisy events but solve different problems.
Standard debounce: a closure holding a timer, cleared and rescheduled on each call, fires after quiet time. On a payment form the risk is real money: debounce on the SUBMIT can drop a click or fire late; the right tools there are disable-on-submit + idempotency keys, not debounce. Debounce validation, not the charge.
Cache by a stable key derived from arguments. Return the cached Promise (so concurrent calls with the same args share one in-flight request — request deduplication). On rejection, evict so retries are possible. For object args, use deep-equality keying (JSON.stringify with sorted keys, or a structured-clone hash). TTL for staleness; capacity bound for memory.
`bind` returns a new function with `this` permanently bound and optionally some pre-applied arguments. Implementation: `Function.prototype.myBind = function(thisArg, ...preset) { const fn = this; return function (...rest) { return fn.apply(thisArg, [...preset, ...rest]); }; }`. Subtlety: bound function called with `new` should ignore thisArg and construct.
`bind` returns a new function with `this` pre-set and optional args pre-applied. Polyfill: closure capturing thisArg + preset args, returning a function that calls the original via `apply`. Handle `new`: when called as constructor, ignore thisArg and use the freshly constructed `this`. Set `bound.prototype = Object.create(fn.prototype)` so instanceof checks work.
Return a new function that calls the original with a fixed `this` and partially applied args. Handle the `new`-as-constructor case so the bound `this` is ignored when called with `new`.
Iterate the array applying the callback with an accumulator. If initial value supplied, start there; else use the first defined element as initial and start from index 1. Skip holes (sparse). Throw TypeError on empty array with no initial. Pass (acc, cur, i, arr) signature.
Object.assign(target, ...sources) copies enumerable own properties from sources onto target, left to right, and returns target. Polyfill: validate target isn't null/undefined, Object() it, loop sources, copy own enumerable keys (use hasOwnProperty), return target. Note it's a shallow copy.
Object.create(proto) makes a new object whose [[Prototype]] is proto. Classic polyfill: a temp constructor function F whose prototype is set to proto, then return new F(). Handle null proto and the optional propertiesDescriptor second argument via Object.defineProperties.
A recursive descent parser: tokenize the JSON string, then parse values by type (object, array, string, number, true/false/null). Handle whitespace, escape sequences, nesting, and throw SyntaxError on malformed input. The realistic answer is explaining the parser structure, not a flawless full implementation.
Recursively serialize by type: primitives (string→quoted, number/boolean→String, null→'null'), arrays → [...], objects → {...} with quoted keys. Skip undefined/functions/symbols in objects (→ omitted) but render them as null in arrays. Handle the gotchas: NaN/Infinity→null, toJSON(), circular refs throw.
There's no pure-JS way to create a real timer — setTimeout is a host API. The realistic 'polyfill' is a wrapper that adds features (cancellable handle, arg forwarding) or a custom scheduler built on requestAnimationFrame comparing timestamps. The interview is about understanding the event loop and that timers are host-provided.
Implement setInterval using recursive setTimeout: schedule a timeout that runs the callback then reschedules itself. This is actually BETTER than native setInterval — it guarantees a full gap between executions and never stacks callbacks. Return a handle with a clear method.
Return a new Promise. Track a results array and a fulfilled count. For each input promise, on fulfill: write the value at its index, increment count, resolve outer when count === length. On reject: reject the outer Promise immediately (short-circuit). Handle the empty-array case (resolve with []) and non-Promise inputs (wrap with Promise.resolve).
Map each input to a promise that resolves with {status:'fulfilled', value} on success or {status:'rejected', reason} on failure. Pass the wrapped array to Promise.all so the outer promise never rejects.
State machine: pending → fulfilled or rejected (transitions once). then/catch/finally chain by returning a new Promise. Resolve thenables. Schedule callbacks as microtasks (queueMicrotask) to match spec timing.
Recursively flatten a nested array. Show the recursive reduce solution, an iterative stack version (no recursion-depth limit), and mention Array.prototype.flat(Infinity) as the built-in. Discuss the depth parameter.
Recurse with reduce + concat for clarity, or iterate with a stack to avoid stack overflow on deep nesting. Support a depth parameter to match Array.prototype.flat semantics.
Walk the array breadth-first: emit all primitives at the current level, queue arrays to process next. This yields top-level values before deeper ones — different from `Array.prototype.flat(Infinity)` which is depth-first. Iterative BFS with a queue is the cleanest implementation.
Iterate once, accumulate counts in an object or Map. reduce((acc, x) => { acc[x] = (acc[x]||0)+1; return acc; }, {}). O(n) time, O(k) space. Discuss object vs Map (Map handles non-string keys, preserves insertion order, no prototype collisions).
Use Array.prototype.reduce for an iterative, idiomatic one-liner, or recursion (head + sum of tail) for the functional approach. Know the tradeoffs: reduce is O(n)/O(1) and safe; naive recursion is O(n) stack space and risks overflow on large inputs.
A node tree: `{ tag, attrs, children, parent }`. Operations: `createNode`, `append`, `remove`, `find` (by tag/id/predicate via DFS), `traverse`, `toHTML` (serialize). Mirror the DOM's parent/child invariants — appending a node detaches it from its previous parent. Useful for templating engines, virtual DOM exercises, and tree manipulation problems.
Method chaining with deferred async actions: each method enqueues an operation and returns `this` synchronously; an internal promise chain awaits each step. Common interview ask: `driver.start().drive(10).stop().wait(5).honk()` — each call appended to a queue, executed in order, with errors propagating cleanly.
A `Fetcher` with an internal `Map<id, value>`. `get(id)` throws if id absent. `post(id, x)` throws if id already exists; otherwise stores. Tests the basics of class state + invariants. Extensions: `update`/`delete`, custom error types, async simulation, typed generics, eviction.
Build a minimal class component model in plain ES5: a Component constructor with setState that re-renders, a tiny VDOM createElement/render pipeline, prototypal inheritance for user components. Demonstrates understanding of prototypes, state-update batching, and the render/diff/commit loop.
Recursively walk the structure; if a value is a function call it (with optional context), replace inline; if an array or object, recurse. Mind cycles with a `WeakSet`; mind async (await thenables) and class instances (don't recurse into them).
Currying via closures: each call returns a function that captures the running product. The trick is the function must also coerce to its value — implement valueOf/toString so mul(2)(3)(4) evaluates to 24 when used as a number.
Return a function that either accepts another argument and returns itself, or returns the running total when called with no argument. Implementation hinges on closure + a base case.
A `Map<eventName, Set<listener>>` with `on`/`off`/`emit`/`once`. `on` returns an unsubscribe function. `emit` calls listeners with the payload (synchronously by default; isolate errors so one bad listener doesn't break the rest). Supports wildcard or namespacing as extensions. Foundation for pub/sub, custom stores, and decoupled module communication.
Map<event, Set<handler>>. on/off/emit, with `on` returning an unsubscribe function. Handle errors per-handler so one throw doesn't break the rest. Bonus: once, namespacing, wildcard.
A pub/sub keeps a Set of subscribers and a state; `subscribe(fn)` returns an unsubscribe; `setState(updater)` produces next state and notifies. A selector layer wraps it: each subscriber registers a selector + equalityFn (default `Object.is`) and is only re-notified when its selected slice changes — the kernel of Redux/Zustand.
Use `reduce` to chain promises: start from `Promise.resolve([])`, await previous, run next factory, append result. Or use a `for...of` loop with `await` — cleaner. Tasks must be passed as factory functions, not pre-created promises (those already started).
A queue with a configurable max concurrency: `add(task)` returns a promise; running count is tracked; when one completes, the next pending task starts. Supports success/error callbacks, custom executors, and `drain` / `idle` events. Core data structure: a FIFO queue + a `running` counter + a `dequeue()` step on settle.
Maintain an active count and a waiting queue. Each enqueue returns a promise; when active < K, run; otherwise wait. On finish, pull the next waiter. Bonus: cancellation, retries, prioritization.
Maintain a counter of running tasks and a FIFO queue of pending ones. `schedule(task)` returns a Promise. If running < max, fire it; else push to queue. On task completion, drain one from the queue. Variants: per-key concurrency, retry, priority, AbortSignal cancellation.
`limitConcurrency(tasks, limit)` runs N workers in parallel; each pulls the next task index until done; returns results in original order. Cleaner than queueing tasks one by one. Pattern: a shared index, N async worker functions started in parallel, each looping until index is past the end.
An interview theme bundling JS internals: closures (functions remembering their lexical scope), the event loop (single thread + queues scheduling async callbacks), Promises (objects representing future values with microtask reactions), and async/await (sugar over Promises). Expect output-prediction puzzles and 'explain how X works under the hood'.