JS Fundamentals at Depth — Cheat Sheet

FE Lead prep · ref 12 · §2.6 · rapid-fire baseline-credibility questions

The frame — lexical vs runtime

Closures / scope / TDZ = LEXICAL (decided by where written).   this / prototype chain = RUNTIME (decided by how called). Different questions, different times.

var / let / const + TDZ

varlet / const
Scopefunctionblock
HoistundefinedTDZ (ReferenceError if read early)
const locks the binding, not the value: const a=[];a.push(1) ✓; a=[] ✗.

Closures

Function + lexical env it was declared in; keeps outer vars alive after outer returns. Powers privacy, factories, useState.
for(var i=0;i<3;i++) setTimeout(()=>log(i)); // 3 3 3
for(let i=0;i<3;i++) setTimeout(()=>log(i)); // 0 1 2
Leak risk: closed-over vars (DOM, arrays) can't be GC'd while closure lives (L08).

this — call site decides (4 rules, high→low)

Callthis
new Fn()new instance
fn.call/apply/bind(o)explicitly o
obj.fn()obj (before the dot)
fn() bareundefined (strict) / global
Arrow = no own this → uses enclosing lexical this (+ no own arguments, can't new). Ideal for callbacks inside a method.
const f = obj.hi; f();          // ✗ undefined — lost this
setTimeout(()=>obj.hi(), 0);     // ✓ arrow keeps call form
setTimeout(obj.hi.bind(obj), 0); // ✓ bind pins this

Prototypes

Objects linked to objects; property read walks the chain until found or null. class = sugar; methods live once on the prototype, shared. extends links prototypes; instanceof checks the chain.

ESM vs CJS (static vs dynamic)

ESM import/exportCJS require
Resolvedstatic, pre-execruntime call
Loadasyncsync
Bindinglivecopied value
Modestrict + top-level awaitsloppy
Tree-shakeyesnot reliably
Why ESM tree-shakes: static structure ⇒ bundler knows used/unused exports without running code ⇒ drops dead. CJS require(x) can be dynamic ⇒ can't prove unused (L05; + sideEffects:false).

Event delegation

list.addEventListener('click', e => {
  const li = e.target.closest('li');   // bubbling + target
  if (li) select(li.dataset.id);       // one handler, all current+future rows
});
Why: 1 listener not N (memory/setup, L07/L08) + auto-covers dynamically added/removed rows (virtualized lists). Caveat: needs bubbling (focus/blur don't → use focusin); filter the target.
3 phases: capture (root→target, top-down) → targetbubble (target→root, bottom-up). addEventListener = bubble by default (why delegation works); {capture:true} fires first (interception). target=what triggered (clicked child) vs currentTarget=where handler is (parent). stopPropagation halts trip but same-element listeners still run; stopImmediatePropagation stops those too. Delegate non-bubbling (focus/blur) via focusin/focusout OR the capture phase.

Lead one-liners (memorize)

Frame  "Closures are lexical — fixed by where written; this is dynamic — fixed by how it's called. Different questions at different times."
Closure  "A function that remembers where it was born. var in a loop shares one binding (3 3 3); let gives each its own (0 1 2)."
this  "Set by the call site, not the definition. obj.fn() binds obj; a bare fn() loses it; arrows borrow the enclosing scope's."
Modules  "ESM is static and analyzable; CJS is a runtime call. That's the whole reason ESM tree-shakes and CJS doesn't."
Lead  "Codify the mechanics into lint rules + conventions — arrow class fields, hooks-deps lint, ESM-only — so the bugs are un-writable, not policed per PR."

Sources: MDN — Closures · MDN — this · MDN — Prototype chain · MDN — TDZ · Node — ESM vs CJS · MDN — Event delegation

Don't fail the interview

1. Closures = lexical (where written); this = runtime (how called). The master distinction.
2. var hoists→undefined; let/const hoist→TDZ (ReferenceError). const = binding not value.
3. Loop: var3 3 3 (one shared binding), let0 1 2 (per-iteration binding).
4. const f=obj.method; f() loses this (bare call). Fix: .bind(obj) or arrow wrapper.
5. Arrow has no own this — borrows enclosing lexical scope. Can't new, no arguments.
6. class methods live once on the prototype (shared), not per-instance. class = sugar.
7. ESM tree-shakes because it's static; CJS require is a runtime call → can't prove dead exports.
8. Delegation: one listener on the parent + closest() — for big/virtualized lists. Not N per-row listeners.
9. Capture (down) → target → bubble (up). Default = bubble (makes delegation work); capture = intercept first. focus/blur don't bubble → focusin or capture phase.