Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

20 chapters
⌕
Beginner15›
00Scouting reportR1Screening callR2Machine codingR3JavaScript & TSR3·TSTypeScriptR4React & Next.jsR5Node & NestJSR6Databases & RedisR7DSA roundR8System designR9AWS, Docker, CI/CDR10Resume grillingR11BehaviouralR12HR & the number✓The week before
Advanced5›
50LWhat changes at ₹50L50LHard DSA50LDistributed systems50LRuntime internals50LStaff behavioural
/ search[ ] chaptert top

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
R3

JavaScript & TypeScript

Length30–60 min
WhoSenior engineer
DecidesWhether six years is real
Fail modeFramework knowledge, no language knowledge
serviceproductsaasagency

The most common way a six-year candidate gets rejected is knowing React deeply and JavaScript shallowly. These are the questions that expose it. Every answer here has a shallow version that every candidate gives and a deep version that almost nobody does — the deep half is what is written out.

3.1
Explain the event loop. Where do promises and setTimeout sit?
What they are really testingThe single most-asked senior JavaScript question. They want the microtask/macrotask distinction, not "JavaScript is single-threaded and non-blocking".

JavaScript runs one call stack. When the stack empties, the runtime drains the microtask queue completely — resolved promise callbacks, queueMicrotask, MutationObserver — and only then takes one macrotask: a timer, an I/O callback, a UI event. Then it drains microtasks again. So microtasks always run before the next macrotask, and a microtask that schedules another microtask can starve the loop entirely.

In Node the macrotask side is split into phases that run in a fixed order:

node event loop phases, in order
timers        // setTimeout / setInterval callbacks
pending       // some system-level callbacks
poll          // I/O — where the loop actually waits
check         // setImmediate
close         // 'close' events (socket.on('close'))

// between EVERY phase transition: drain nextTick queue, then microtasks
the ordering question they will actually ask
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
process.nextTick(() => console.log('4'))
queueMicrotask(() => console.log('5'))
console.log('6')

// 1 6 4 3 5 2
// sync first (1,6), then nextTick (4) — its own queue, ahead of
// promises — then microtasks in scheduling order (3,5), then timers (2)
The answer that loses the room

"Promises go to the callback queue and setTimeout goes to the callback queue, and the event loop picks them in order." That is the tutorial answer and it is wrong — there are two queues with different priorities, and the whole question exists to find out whether you know that.

Two extras that make you sound like you have debugged this rather than read it: setTimeout(fn, 0) is clamped to roughly 1ms and nested timers get clamped to 4ms after five levels; and in the browser, rendering happens between macrotasks, which is why a long microtask chain freezes the page while a chain of setTimeouts does not.

They will push with
  • Why does an infinite promise chain freeze the browser but an infinite setTimeout chain does not?
  • Difference between setImmediate and setTimeout(fn, 0) in Node?
  • Where does async/await sit in this model?
3.2
What actually happens when you await?
What they are really testingWhether you understand async/await as syntax over promises, or think it is a new concurrency primitive.

async makes a function return a promise. await suspends the function, registers the rest of the body as a .then callback on the awaited value, and returns control to the caller. The continuation therefore runs as a microtask — which is why the code after an await never runs synchronously, even when you await a value that is already resolved.

async function f() {
  console.log('a')
  await null          // even a non-promise: still yields to the microtask queue
  console.log('b')    // this is a microtask continuation
}
f()
console.log('c')
// a c b

The practical consequence is sequencing. Two independent awaits run one after the other; if they do not depend on each other, that is wasted latency:

They will push with
  • Rewrite this to run them in parallel.
  • What happens if one of them rejects?
  • Does await block the event loop?
3.3
These two look the same. Which is faster and why?
What they are really testingAccidental sequential awaits — one of the most common real performance bugs in Node codebases.

The three calls are independent, so the first version pays 600ms for 200ms of work. Note the subtlety worth saying out loud: the promises in Promise.all start executing the moment they are created, not when they are awaited — so even const a = getUser(); const b = getPosts(); await a; await b; is concurrent. It is the await on the call itself that serialises.

sequential — 600ms
const user  = await getUser(id)      // 200ms
const posts = await getPosts(id)     // 200ms — waits for the line above for no reason
const stats = await getStats(id)     // 200ms
concurrent — 200ms
const [user, posts, stats] = await Promise.all([
  getUser(id), getPosts(id), getStats(id)
])
The answer that loses the room

Blindly converting every sequential await to Promise.all. If getPosts needs the user id from getUser, they are genuinely dependent and must be sequential. Show that you check the dependency before you parallelise.

They will push with
  • What if one fails and you still want the others?
  • How would you limit this to 5 concurrent when there are 500?
3.4
Promise.all vs allSettled vs race vs any.
What they are really testingWhether you have chosen between these in production rather than memorised four names.
CombinatorSettles whenReach for it when
allAll fulfil, or the first rejectsYou need every piece — a dashboard that is meaningless with a missing panel.
allSettledAll settle, never rejectsPartial success is acceptable — fanning out to three third-party providers where one being down should not fail the request.
raceFirst to settle, either wayTimeouts. Race the work against a rejecting timer.
anyFirst to fulfil; rejects only if all rejectRedundant sources — three mirrors, take whichever answers first.
the timeout pattern, and why AbortController matters
// race gives you the timeout, but the losing request keeps running
const withTimeout = (p, ms) => Promise.race([
  p,
  new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms))
])

// promises are NOT cancellable. To actually stop the work:
const ac = new AbortController()
setTimeout(() => ac.abort(), 2000)
const res = await fetch(url, { signal: ac.signal })

// modern shorthand for exactly this:
await fetch(url, { signal: AbortSignal.timeout(2000) })

The line that scores: "none of these cancel anything — Promise.all rejecting does not stop the sibling requests, they run to completion and their results are discarded. JavaScript promises are not cancellable, which is the entire reason AbortController exists."

They will push with
  • Implement Promise.all from scratch.
  • How do you cancel an in-flight fetch on unmount?
  • What does allSettled return for a rejected entry?
3.5
Implement Promise.all from scratch.
What they are really testingAsked constantly at senior level. The two failure points are preserving index order and handling the empty array.

Three things to narrate while writing it: out[i] = v rather than out.push(v), because push gives you completion order not input order; the empty-array early return, because otherwise it never resolves; and that calling reject more than once is harmless, since a promise settles once and later calls are ignored.

function all(promises) {
  return new Promise((resolve, reject) => {
    const items = Array.from(promises)
    const out = new Array(items.length)
    let done = 0

    if (items.length === 0) return resolve([])   // the case people miss

    items.forEach((p, i) => {
      // Promise.resolve handles non-promise values too
      Promise.resolve(p).then(v => {
        out[i] = v                                // index, not push
        if (++done === items.length) resolve(out)
      }, reject)                                  // first rejection wins; later ones are no-ops
    })
  })
}
They will push with
  • Now write allSettled.
  • Now write a version that limits concurrency to N.
3.6
Write a promise pool that runs at most N tasks concurrently.
What they are really testingGenuinely useful and genuinely common at senior level — rate-limited third-party APIs, bulk imports, image processing.

The detail that separates a good answer from a great one: tasks must be thunks. If you pass an array of promises they have already started and the pool controls nothing. Say that unprompted.

The other detail: this is a worker-pull design, not batching. Chunking into groups of five and awaiting each group means the whole group waits for its slowest member — with N workers pulling from a shared cursor, a fast worker immediately picks up the next item.

async function pool(tasks, limit = 5) {
  const results = new Array(tasks.length)
  let next = 0

  async function worker() {
    while (next < tasks.length) {
      const i = next++          // grab an index, then release
      results[i] = await tasks[i]()
    }
  }

  // N workers all pulling from the same index — no batching stalls
  await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker))
  return results
}

// tasks are FUNCTIONS returning promises, not promises —
// a promise has already started, so it cannot be throttled
await pool(urls.map(u => () => fetch(u)), 5)
They will push with
  • What if one task throws?
  • Add a retry with backoff.
  • How would you preserve order if tasks finish out of order?
3.7
Write retry with exponential backoff.
What they are really testingWhether you know that naive retries make outages worse.

Two things they are listening for. Jitter: without it, a thousand clients that failed together retry together and re-create the exact spike that caused the outage — this is the thundering herd, and randomising the delay is the fix. And retryability: a 400 or a 404 will never succeed on retry, so retrying it just burns time and quota. Retry timeouts, 429s and 5xx; fail fast on 4xx.

async function retry(fn, { tries = 4, base = 300, factor = 2, jitter = true } = {}) {
  let lastErr
  for (let i = 0; i < tries; i++) {
    try { return await fn() }
    catch (err) {
      lastErr = err
      if (!isRetryable(err) || i === tries - 1) throw err
      const wait = base * factor ** i
      const delay = jitter ? wait * (0.5 + Math.random()) : wait
      await new Promise(r => setTimeout(r, delay))
    }
  }
  throw lastErr
}

// don't retry what will never succeed
const isRetryable = e =>
  e.name === 'TimeoutError' || [429, 502, 503, 504].includes(e.status)
They will push with
  • Where does a circuit breaker fit relative to this?
  • What do you do after the last retry fails?
  • Is retrying a POST safe?
3.8
What is a closure? Give me one from your own code.
What they are really testingEveryone has the definition. Almost nobody has the memory-leak half.

A closure is a function together with the scope it was defined in, kept alive after that scope has returned. The definition is the cheap part — go straight to a real one:

function debounce(fn, ms) {
  let timer                       // closed over, private, survives every call
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => fn(...args), ms)
  }
}
the leak
// BAD — every call adds a listener that captures the payload forever
function handle(req) {
  const bigPayload = req.body
  emitter.on('tick', () => log(bigPayload.id))
}

// FIX — remove it, or use once()
function handle(req) {
  const onTick = () => log(req.body.id)
  emitter.on('tick', onTick)
  req.on('close', () => emitter.off('tick', onTick))
}

Then the senior half: closures are the main way you leak memory in a long-lived Node process. A closure that captures a large object keeps it unreachable-for-collection as long as the returned function is referenced. The classic production leak is an event listener that closes over a request context and is never removed — every request adds a listener, each pinning its own context, and the heap climbs until the process dies.

They will push with
  • How would you find that leak in production?
  • What does a WeakMap solve here?
  • Why does a loop with var and setTimeout print the same number?
3.9
Why does this print 3, 3, 3 — and what are the two fixes?
What they are really testingScope and closures together. Still asked because it separates people who understand binding from people who memorised "use let".

var is function-scoped, so all three callbacks close over the same binding. The loop finishes before any timer fires, and by then that single i is 3.

Two fixes, and knowing why the first works is the actual answer:

for (var i = 0; i < 3; i++) setTimeout(() => console.log(i))
// 3 3 3
// 1. let — a NEW binding per iteration, which the spec creates deliberately
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i))   // 0 1 2

// 2. an IIFE capturing the value — how everyone did it before ES6
for (var i = 0; i < 3; i++) (j => setTimeout(() => console.log(j)))(i)
They will push with
  • Does the same happen with a for…of loop?
  • What about const in a for loop?
3.10
Explain this. How is an arrow function different?
What they are really testingWhether you know that this is decided at call time, not at definition time.

this is determined by how the function is called, and there are exactly five rules, checked in this order:

  1. new Fn() → the newly created object.
  2. fn.call(x) / apply / bind → whatever you passed.
  3. obj.fn() → obj, the thing before the dot.
  4. Plain fn() → undefined in strict mode and modules, globalThis in sloppy mode.
  5. Arrow function → none of the above. Arrows have no this binding at all; they resolve it lexically from the enclosing scope, and bind cannot change it.
the losing-this bug, and the three fixes
class Counter {
  count = 0
  inc() { this.count++ }
  incArrow = () => { this.count++ }   // class field: lexical this
}
const c = new Counter()
const f = c.inc
f()                       // TypeError — this is undefined, the dot is gone

f.call(c)                 // fix 1
const g = c.inc.bind(c)   // fix 2
const h = c.incArrow      // fix 3 — works detached

And the flip side that shows judgement: an arrow is wrong as an object method or on a prototype, because there is no dynamic this to pick up the instance — const o = { n: 1, get: () => this.n } is always broken.

They will push with
  • What is this inside a plain callback passed to forEach?
  • Why do class methods need bind in React class components but not with arrow fields?
  • Implement bind yourself.
3.11
Implement bind.
What they are really testingCombines this-binding, closures, rest args and — if you go all the way — the new case.

Most candidates stop at fn.apply(ctx, [...bound, ...args]), which is a fine answer. The new handling is what gets you remembered — a bound function used as a constructor is supposed to ignore the bound this.

Function.prototype.myBind = function (ctx, ...bound) {
  const fn = this
  if (typeof fn !== 'function') throw new TypeError('not callable')

  function wrapper(...args) {
    // if called with new, ignore ctx and use the fresh instance
    const calledWithNew = this instanceof wrapper
    return fn.apply(calledWithNew ? this : ctx, [...bound, ...args])
  }
  wrapper.prototype = Object.create(fn.prototype || null)
  return wrapper
}
They will push with
  • What does partial application mean here?
  • Can you bind an arrow function?
3.12
Explain the prototype chain.
What they are really testingWhether "class" means anything to you beyond syntax.

Every object has an internal link ([[Prototype]], reachable via Object.getPrototypeOf) to another object. Property lookup walks that chain until it finds the key or hits null. class is syntax over this: methods live on Constructor.prototype and are shared by every instance, which is why defining methods inside the constructor body instead wastes one function object per instance.

class A { hi() {} }          // hi lives once, on A.prototype
function B() { this.hi = () => {} }   // a new closure per instance

const a1 = new A(), a2 = new A()
a1.hi === a2.hi              // true

// a prototype-free object is the correct shape for a lookup map:
const map = Object.create(null)
map.toString                 // undefined — no inherited keys to collide with
({}).toString                // function — which is why {} as a map is a bug waiting

__proto__ is the (deprecated) accessor for the link; prototype is a property that only functions have, and it is the object that will become the [[Prototype]] of instances they construct. Getting that distinction right in one sentence is most of the marks.

They will push with
  • What is prototype pollution and how do you prevent it?
  • Difference between __proto__ and prototype?
  • How does instanceof work?
3.13
var, let, const, and what is the temporal dead zone?
What they are really testingHoisting. A one-line answer here is fine — a wrong one is fatal.

var is function-scoped and hoisted initialised to undefined. let and const are block-scoped and hoisted uninitialised — the span between the top of the block and the declaration is the temporal dead zone, and touching the binding there throws a ReferenceError rather than silently giving undefined. That is the entire point of the TDZ: it turns a silent bug into a loud one.

const prevents rebinding, not mutation. const a = []; a.push(1) is legal. For real immutability you need Object.freeze, and that is shallow.

console.log(v)   // undefined — hoisted and initialised
var v = 1

console.log(l)   // ReferenceError: Cannot access 'l' before initialization
let l = 1

typeof undeclared   // "undefined" — safe
typeof l            // ReferenceError if l is in its TDZ — the one place typeof throws
They will push with
  • Are function declarations hoisted differently from function expressions?
  • Why is const the default in modern code?
3.14
Deep clone an object. What are the failure modes?
What they are really testingWhether you still reach for the JSON trick and whether you know what it destroys.

structuredClone(obj) is the modern answer — built into browsers and Node, handles Date, Map, Set, RegExp, typed arrays, ArrayBuffer and circular references.

The answer most candidates give, JSON.parse(JSON.stringify(x)), silently destroys a lot:

const src = {
  d: new Date(), m: new Map([['a',1]]), s: new Set([1]),
  u: undefined, f: () => {}, n: NaN, i: Infinity, big: 10n
}
JSON.parse(JSON.stringify(src))
// d   → "2026-09-05T..."  a string, not a Date
// m,s → {}               emptied
// u,f → dropped entirely (keys disappear)
// n,i → null
// big → throws TypeError
// circular → throws
hand-rolled, cycle-safe
function clone(v, seen = new WeakMap()) {
  if (v === null || typeof v !== 'object') return v
  if (seen.has(v)) return seen.get(v)          // the cycle guard
  const out = Array.isArray(v) ? [] : Object.create(Object.getPrototypeOf(v))
  seen.set(v, out)
  for (const k of Reflect.ownKeys(v)) out[k] = clone(v[k], seen)
  return out
}

What structuredClone still cannot do: functions, DOM nodes, class prototypes (you get a plain object back, not an instance), and getters/setters. If you need those, a hand-written recursive clone with a WeakMap of already-seen objects is the answer — and the WeakMap is what handles cycles.

They will push with
  • Why WeakMap and not Map here?
  • What is a shallow clone and when is it enough?
3.15
Implement debounce and throttle, and tell me where you used each.
What they are really testingEveryone can define them. Few can write throttle correctly or name a real use.

Debounce waits for the input to stop: search-as-you-type, autosave, resize-then-recalculate. Throttle guarantees a maximum rate: scroll handlers, drag, mousemove, analytics pings.

Concrete answer for "where did you use it": the faceted search on your camp-booking marketplace was debounced at 300ms — without it, filtering across location, age, interest and price fired a request per keystroke per facet.

debounce — waits for silence
function debounce(fn, ms) {
  let t
  const wrapped = (...a) => {
    clearTimeout(t)
    t = setTimeout(() => fn(...a), ms)
  }
  wrapped.cancel = () => clearTimeout(t)    // needed for cleanup on unmount
  return wrapped
}
throttle — guarantees a maximum rate
function throttle(fn, ms) {
  let last = 0, timer = null, lastArgs
  return (...a) => {
    const now = Date.now()
    lastArgs = a
    if (now - last >= ms) { last = now; fn(...a) }
    else if (!timer) {
      // trailing call, so the final event is not swallowed
      timer = setTimeout(() => {
        timer = null; last = Date.now(); fn(...lastArgs)
      }, ms - (now - last))
    }
  }
}
The answer that loses the room

A throttle with no trailing call. The naive version drops the last event, so a user who stops scrolling mid-gesture never gets the final position and the UI ends up out of sync. Mentioning the trailing edge unprompted is the difference here.

They will push with
  • Which one would you use for an autosave?
  • How do you cancel a pending debounce when a component unmounts?
  • requestAnimationFrame vs throttle for scroll?
3.16
Explain event bubbling, capturing and delegation.
What they are really testingDOM fundamentals, and whether you understand what React is doing under its synthetic events.

An event travels down from the root to the target (capture phase), fires on the target, then travels back up (bubble phase). Listeners default to the bubble phase; pass { capture: true } for the way down.

Delegation is putting one listener on a common ancestor and working out which descendant was hit — one listener instead of a thousand, and it keeps working for rows added to the DOM later:

list.addEventListener('click', (e) => {
  const row = e.target.closest('[data-id]')     // not e.target directly
  if (!row || !list.contains(row)) return
  open(row.dataset.id)
})

e.target is what was actually clicked (possibly a span inside the row); e.currentTarget is the element the listener is on. Using closest() instead of e.target is what makes delegation robust against nested markup.

Also distinguish stopPropagation() (stop travelling) from preventDefault() (stop the browser's default action) — they are unrelated and candidates mix them up constantly. And note that some events do not bubble: focus, blur, load, mouseenter — which is why focusin and focusout exist.

They will push with
  • How does React attach its events?
  • How do you delegate a focus event?
  • What does passive: true do on a scroll listener?
3.17
== vs ===. Is there any legitimate use of ==?
What they are really testingCoercion. Also whether you speak in absolutes or in judgement.

=== compares type and value. == applies the abstract equality algorithm, which coerces first — null == undefined is true, '1' == 1 is true, [] == false is true.

There is exactly one idiom worth keeping: x == null is true for precisely null and undefined and nothing else. It is the shortest correct nullish check and it is genuinely useful. Everywhere else, ===.

NaN === NaN            // false — use Number.isNaN or Object.is
Object.is(NaN, NaN)    // true
Object.is(0, -0)       // false — the other case Object.is differs on

0 == ''                // true
null == 0              // false  (null only equals undefined)
[] == ![]              // true   — the party trick
They will push with
  • How does Object.is differ from ===?
  • What does the + operator do with an object?
  • Why is typeof null "object"?
3.18
Map vs Object. Set vs Array. When would you use a WeakMap?
What they are really testingWhether you pick data structures on purpose.
ObjectMap
KeysStrings and symbols onlyAnything, including objects and NaN
OrderInteger-like keys sort first — surprisingInsertion order, always
SizeObject.keys(o).length — O(n)map.size — O(1)
Inherited keysYes, unless Object.create(null)Never
JSONSerialises directlyDoes not — needs conversion

Rule of thumb: Map for a collection you add to and delete from at runtime; object for a fixed-shape record you will serialise.

Set gives O(1) membership against Array.includes at O(n) — this is the standard fix when a filter containing an includes turns quadratic and a page hangs at ten thousand rows.

the quadratic bug and its one-line fix
// O(n·m) — 10k × 10k = 100 million comparisons
const missing = all.filter(x => !existing.includes(x.id))

// O(n + m)
const have = new Set(existing)
const missing = all.filter(x => !have.has(x.id))

WeakMap holds its keys weakly: an entry disappears when nothing else references the key object. Use it to attach metadata to objects you do not own — caches keyed by an object, per-instance private data, or the cycle-guard in a deep clone — without preventing garbage collection. It is not enumerable and has no size, precisely because entries can vanish at any moment.

They will push with
  • Why can a WeakMap key not be a string?
  • How would you build an LRU cache with a Map?
  • What order does Object.keys return?
3.19
What does reduce actually do? Write groupBy with it.
What they are really testingWhether you can use reduce for something other than summing an array.

Say the accumulator sentence: reduce folds a collection into a single value by threading an accumulator through, and the accumulator can be any shape — a number, an object, a Map, another array.

const groupBy = (arr, keyOf) =>
  arr.reduce((acc, item) => {
    const k = keyOf(item)
    ;(acc[k] ||= []).push(item)
    return acc
  }, {})

groupBy(bookings, b => b.status)
// { pending: [...], confirmed: [...] }
2026 note

Modern runtimes have Object.groupBy(arr, fn) and Map.groupBy built in. Mentioning that you would reach for the built-in and only hand-roll for older targets is a small, cheap credibility win.

They will push with
  • Rewrite it to return a Map.
  • When is reduce the wrong choice? (When a for…of is clearer — say so.)
3.20
ESM vs CommonJS — and why does import hoist but require not?
What they are really testingModule systems. Comes up constantly in Node interviews because teams are still mid-migration.

require is a runtime function call: it executes wherever it appears, resolves synchronously, and returns a value you can compute — require(cond ? 'a' : 'b') is legal. import is a static declaration: the specifiers are parsed before any code runs, which is what makes the module graph knowable ahead of execution.

Three consequences worth naming:

  • Hoisting. All imports are resolved and evaluated before the importing module's body runs.
  • Live bindings. ESM imports are references to the exporting module's binding, not copies. If the exporter reassigns, the importer sees the new value. CommonJS copies the value at require time.
  • Tree shaking. Only possible because the graph is static — a bundler can prove an export is unused. CommonJS cannot be shaken reliably.
// ESM live binding
// counter.js
export let n = 0
export const inc = () => n++

// main.js
import { n, inc } from './counter.js'
inc(); console.log(n)     // 1 — CommonJS would print 0

// top-level await: ESM only
const cfg = await loadConfig()

Also know the interop rule, because it bites in real projects: ESM can import CommonJS (the whole module.exports arrives as the default export), but CommonJS cannot require an ESM module — it must use dynamic import(), which is async. That asymmetry is why so many Node codebases stall halfway through the migration.

They will push with
  • What breaks tree shaking? (Side effects, barrel files, CommonJS.)
  • What does "type": "module" do in package.json?
  • What is a side-effectful import?
3.21
What is tree shaking and what silently breaks it?
What they are really testingBundle size awareness — directly relevant to your page-load work.

Tree shaking is dead-code elimination over the static ESM graph: the bundler proves an export is never used and drops it. Four things break it:

  • Side effects. If a module does work at import time, the bundler cannot prove removing it is safe. Declaring "sideEffects": false in package.json (or listing the files that do have them) is what tells it otherwise.
  • CommonJS. Dynamic requires cannot be statically analysed.
  • Barrel files. An index.ts re-exporting everything makes one import pull the whole directory into the graph. This is the most common real cause and it is worth naming, because it is also a build-speed problem.
  • Namespace imports. import * as _ from 'lodash' defeats it; import debounce from 'lodash/debounce' does not.
They will push with
  • How would you find what is making a bundle large?
  • What is code splitting and how does it differ from tree shaking?
3.22
Rapid-fire JavaScript — one clean sentence each
What they are really testingBreadth. Service-company rounds run twenty of these in thirty minutes and score you on speed, not depth.
  • null vs undefined. undefined means never assigned; null is an assigned "nothing". Only null is intentional.
  • Why is typeof null === "object"? A bug from 1995 kept for backwards compatibility.
  • Hoisting of functions. Function declarations are hoisted whole and callable before their line; function expressions assigned to var are undefined until the assignment runs.
  • Currying. const add = a => b => c => a + b + c — one argument at a time, returning a function until saturated.
  • IIFE. Pre-module scope isolation. With ESM it is largely obsolete; blocks and modules do the job.
  • ?? vs ||. || falls through on any falsy value, so 0 || 10 is 10 — a real bug with counts and prices. ?? only falls through on null/undefined.
  • Optional chaining. a?.b?.() short-circuits to undefined instead of throwing. It does not protect against a missing variable, only a nullish property.
  • Generators. Functions that can pause and resume with yield. Real sighting: Redux Saga, and lazy infinite sequences.
  • Object.freeze. Shallow — nested objects stay mutable. Deep freeze needs recursion.
  • Symbol. A guaranteed-unique property key. Used for metadata that must not collide, and for protocol hooks like Symbol.iterator.
  • Iterators. Anything with a [Symbol.iterator] works with for…of and spread. That is how you make a custom class spreadable.
  • Event delegation vs direct binding. Fewer listeners, works for future nodes.
  • Pass by value or reference? Always by value — but for objects the value is a reference.
  • slice vs splice. slice returns a copy and does not mutate; splice mutates in place and returns what it removed.
  • for…in vs for…of. in walks enumerable keys including inherited ones; of walks values of an iterable. Almost never use for…in on an array.
←previousMachine coding↑ CovernextTypeScript→