02 · JavaScript Deep Dive
The language and its runtime, from the type system up through the engine that executes it. Frameworks are conventions on top of this; mastering the language is what makes a senior engineer framework-agnostic.
Positioning
React, Next, and every store library are JavaScript. The bugs that survive to production are almost always language-level: a stale closure, a this binding, an unhandled promise rejection, a reference held alive by a forgotten listener. This file builds the language from primitives to the engine.
Foundations: the execution model
JavaScript is a single-threaded, garbage-collected, prototype-based, dynamically-typed language with first-class functions and an event-loop concurrency model. Each of those words is load-bearing:
- Single-threaded — one call stack; only one thing executes at a time. Concurrency comes from the event loop offloading async work and resuming via callbacks/microtasks, not from threads (real threads require Workers; see
04). - Garbage-collected — you don’t free memory; the engine reclaims unreachable objects. “Unreachable” is the key word (see Memory).
- Prototype-based — objects delegate to other objects via a prototype chain; there are no classes at the runtime level, only syntactic sugar over prototypes.
- Dynamically typed — values have types, variables don’t. Types are checked at runtime.
- First-class functions — functions are values: passed, returned, stored. This is what makes closures and the functional patterns in React possible.
Deep dive
1. Types and coercion
Seven primitives: string, number, boolean, null, undefined, symbol, bigint. Everything else is an object (including arrays and functions). Primitives are immutable and copied by value; objects are referenced.
numberis IEEE-754 double — hence0.1 + 0.2 !== 0.3, and integer safety only up toNumber.MAX_SAFE_INTEGER(2^53−1); beyond that usebigint.typeof null === 'object'is a historic bug, preserved forever.==does coercion (avoid);===does not (default). The coercion rules are a minefield:[] == ![]istrue,null == undefinedistruebutnull == 0isfalse. Memorize the few useful ones (x == nullchecks null-or-undefined) and use===everywhere else.- Truthiness: falsy values are
false, 0, -0, 0n, "", null, undefined, NaN. Everything else is truthy (including[]and{}). The??(nullish coalescing) operator only falls through onnull/undefined, unlike||which falls through on any falsy — a crucial distinction for0and"". - Boxing:
"abc".lengthworks because the primitive is temporarily wrapped in aStringobject.
2. Scope, hoisting, and the TDZ
- Lexical scoping: a function’s scope is determined by where it’s written, not where it’s called. This is the basis of closures.
varis function-scoped and hoisted (initialized toundefined).let/constare block-scoped and hoisted but not initialized — accessing them before declaration throws (the Temporal Dead Zone). Always useconstby default,letwhen reassigning, nevervar.- Hoisting: declarations are processed before execution. Function declarations are fully hoisted (callable before their line); function expressions/arrows are not.
3. Closures
A closure is a function bundled with references to its lexical environment. The function “remembers” the variables in scope where it was defined, even after that scope has returned.
function counter() {
let count = 0; // captured by the returned closure
return () => ++count; // each call mutates the captured `count`
}
const next = counter();
next(); // 1
next(); // 2 — the environment persists
Closures power: data privacy (module pattern), memoization, currying, and every React hook (useState’s setter closes over the fiber). They’re also the #1 source of bugs: a stale closure captures an old value. In React, a useEffect with a missing dependency captures the variable from the render it was created in — the value is “frozen.” The fix is correct dependency arrays or refs.
// Classic stale-closure trap
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // logs 3, 3, 3 — one shared `i`
}
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // logs 0, 1, 2 — fresh binding per iteration
}
4. this, call/apply/bind, and arrow functions
this is determined by how a function is called, not where it’s defined (except arrows). The rules, in precedence order:
new—thisis the freshly created object.- Explicit —
fn.call(obj),fn.apply(obj, args),fn.bind(obj)setthistoobj. - Implicit —
obj.method()setsthistoobj. Detaching the method (const m = obj.method; m()) loses it. - Default —
undefinedin strict mode,globalThisotherwise.
Arrow functions have no own this — they capture this lexically from the enclosing scope. This is exactly why arrows are the default for callbacks in class components and event handlers, and why you must not use an arrow for an object method that needs dynamic this or for a prototype method.
5. Prototypes and the prototype chain
Every object has an internal [[Prototype]] (accessible via Object.getPrototypeOf / the legacy __proto__). Property lookup walks the chain until found or it hits null. class is sugar: methods live on Class.prototype, instances delegate to it.
class Animal { speak() { return 'generic'; } }
class Dog extends Animal { speak() { return 'woof'; } }
const d = new Dog();
// d → Dog.prototype → Animal.prototype → Object.prototype → null
d.speak(); // 'woof' (found on Dog.prototype)
d.toString(); // found on Object.prototype
Understand this to grok inheritance, instanceof (walks the chain), monkey-patching, and why mutating built-in prototypes is dangerous.
6. The event loop, microtasks, and macrotasks
The runtime has: a call stack, a heap, a macrotask (task) queue, and a microtask queue. The loop:
- Run the current task (a script, an event callback) to completion — JS is run-to-completion; it never preempts mid-task.
- Drain the entire microtask queue (promise
.then/awaitcontinuations,queueMicrotask,MutationObserver). - Render if needed.
- Take the next macrotask (
setTimeout, I/O, message, event) and repeat.
The decisive consequence: microtasks always run before the next macrotask and before rendering. A runaway microtask loop can starve rendering entirely.
console.log('1: sync');
setTimeout(() => console.log('4: macrotask'), 0);
Promise.resolve().then(() => console.log('3: microtask'));
console.log('2: sync');
// Order: 1, 2, 3, 4
await x is sugar for .then — code after an await is a microtask continuation. This is why await “pauses” without blocking the thread.
7. Asynchrony: callbacks → promises → async/await
- Promises are state machines:
pending → fulfilled | rejected, settled once..then/.catch/.finallychain; rejections propagate down the chain. async/awaitis syntactic sugar over promises that reads sequentially.try/catchworks naturally.- Concurrency combinators:
Promise.all(all succeed or first reject),Promise.allSettled(wait for all, never reject),Promise.race(first to settle),Promise.any(first to fulfill). - Sequential vs parallel — the most common async perf bug:
// Sequential: total = a + b (slow)
const x = await fetchA();
const y = await fetchB();
// Parallel: total = max(a, b) (start both, then await)
const [x2, y2] = await Promise.all([fetchA(), fetchB()]);
- Cancellation:
AbortController+AbortSignalis the standard for cancelling fetches and timers (React Query, fetch, and event listeners all accept signals). Unhandled rejections should be caught globally (window.onunhandledrejection).
8. Modules
- ESM (
import/export) is the standard: static, statically analyzable (enables tree-shaking), async-loaded, strict-mode by default, with live bindings (an imported binding reflects the exporter’s current value, unlike CommonJS’s copied value). Top-levelawaitis allowed. - CommonJS (
require/module.exports) is Node’s legacy synchronous system: dynamic, value-copied exports, no static tree-shaking. Interop is a perennial pain (esModuleInterop, dual packages, the “ESM/CJS” wars). - Dynamic
import()returns a promise — the basis of route-level and component-level code splitting (React.lazy). See14-build-tools-and-bundlers.md.
9. Memory model and leaks
The heap holds objects; the GC (V8 uses a generational mark-and-sweep with a young “scavenger” and old-space collector) reclaims unreachable objects. Reachability is from roots (the global object, the active call stack). Common leaks in frontend apps:
- Forgotten listeners / subscriptions — an event listener or store subscription keeps its closure (and everything it captures) alive. Always clean up (React
useEffectcleanup). - Detached DOM nodes — a JS reference to a removed DOM node keeps the whole subtree alive.
- Closures over large data — a small callback that closes over a huge array pins that array.
- Growing caches /
Maps — unbounded caches. UseWeakMap/WeakRef/FinalizationRegistrywhen you want keys that don’t prevent collection.
Diagnose with DevTools Memory panel: heap snapshots, allocation timelines, and the “Detached” filter.
10. The engine pipeline (V8) and writing fast JS
V8: Ignition interprets bytecode → Sparkplug baseline-compiles hot-ish code → Maglev/TurboFan optimizes very hot code with speculative assumptions → deoptimization falls back when assumptions break (e.g., an object’s shape changes, or a number becomes a string). Practical levers:
- Keep object shapes stable — initialize all properties in the constructor in the same order; don’t add/delete properties later. V8’s hidden classes make property access fast only when shapes are predictable; inline caches speed up monomorphic call sites and slow down when polymorphic.
- Avoid de-opt triggers in hot loops — mixing types,
try/catchin the hot path (historically),argumentsleaks, sparse arrays. - Don’t micro-optimize cold code. Profile first; most perf wins are algorithmic or in the DOM/network, not in the JS engine.
11. Idioms worth internalizing
- Immutability — structural sharing, spread,
structuredClone()for deep clones, libraries like Immer for ergonomic immutable updates (underpins Redux Toolkit). - Iterators & generators —
function*,yield, lazy sequences; the protocol behindfor...of. - Destructuring, spread/rest, optional chaining (
?.), nullish coalescing (??) — modern ergonomics. Map/Set/WeakMap/WeakSet— proper collections;Mapover object-as-dictionary for non-string keys and frequent add/delete.Proxy/Reflect— metaprogramming; the engine behind Valtio and Vue’s reactivity (see06).
Worked example: a debounced, cancelable async search
function debounce(fn, ms) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), ms); // closure over `t`
};
}
function makeSearcher(endpoint) {
let controller; // closure-private mutable state
return debounce(async (query) => {
controller?.abort(); // cancel the in-flight request
controller = new AbortController();
try {
const res = await fetch(`${endpoint}?q=${encodeURIComponent(query)}`,
{ signal: controller.signal });
return await res.json();
} catch (e) {
if (e.name !== 'AbortError') throw e; // ignore intentional cancels
}
}, 300);
}
This one snippet exercises closures, this-free arrows, promises, AbortController, and the microtask/macrotask interplay (the setTimeout macrotask schedules the await-driven microtask chain).
Pitfalls & gotchas
==coercion; floating-point equality;typeof null.- Stale closures in callbacks and effects (capture vs. live value).
- Losing
thisby detaching methods; using arrows where dynamicthisis needed. - Sequential
awaits that should bePromise.all. - Unhandled promise rejections silently swallowing errors.
- Mutating shared objects/arrays passed by reference (defensive copies, immutability).
- Leaks from un-removed listeners and detached nodes.
- Mutating built-in prototypes.
Interview / self-test questions
- Explain the event loop including microtask vs macrotask ordering. Predict the output of an interleaved
setTimeout/Promise.then/awaitsnippet. - What is a closure, and give a real bug caused by one. (Stale capture; React effect with missing deps.)
- How is
thisdetermined? Contrast normal functions and arrows. nullvsundefined;??vs||;==vs===.- ESM vs CommonJS — name three concrete differences. (Static vs dynamic, live bindings vs copies, tree-shaking, async vs sync, strict-by-default.)
- How does prototypal inheritance work and what does
classdesugar to? - Name three common memory leaks in a SPA and how you’d find them.
- How do you run two async operations in parallel and handle partial failure? (
Promise.allSettled.) - What is the TDZ?
- Why does keeping object shapes stable help V8?
Recommendations
constby default;===always;??/?.liberally; nevervar.- Reach for
Promise.all/allSettledreflexively for independent async work. - Always pair subscriptions/listeners/timers with cleanup.
- Prefer immutable updates; isolate mutation.
- Learn to read a DevTools heap snapshot and flame chart before you need to.
- Lint with ESLint +
eslint-plugin-promiseand TypeScript’s strictness as a forcing function.
Books & references
- “You Don’t Know JS Yet” (2nd ed) — Kyle Simpson (free on GitHub). The deepest free treatment of scope, closures,
this, types, and async. Read Scope & Closures and Objects & Classes first. - “Eloquent JavaScript” — Marijn Haverbeke (free, eloquentjavascript.net). Best from-scratch book; great on higher-order functions and async.
- “JavaScript: The Definitive Guide” (7th ed) — David Flanagan. The reference.
- “Secrets of the JavaScript Ninja” (2nd ed) — Resig & Bibeault. Closures, prototypes, generators, metaprogramming.
- MDN — the authoritative per-API docs and the canonical event-loop and memory-management explainers.
- “What the heck is the event loop anyway?” — Philip Roberts (JSConf talk); “In The Loop” — Jake Archibald (JSConf). The two best talks on the loop, including the microtask/macrotask/render interleave.
- v8.dev/blog — hidden classes, inline caches, the compiler pipeline.
- TC39 proposals (github.com/tc39/proposals) — where the language is going.
Connections
01-browser-engines-and-rendering.md— where the event loop and main thread live; rendering interleave.03-typescript.md— types layered on this language.04-the-web-platform.md— Workers (real threads),fetch,AbortController, observers.05-react-internals-and-patterns.md— hooks as closures; the scheduler as a userland event loop.06-state-management-and-stores.md—Proxy(Valtio), immutability (Immer/Redux), subscriptions.