Skip to content
JavaScript Deep Dive

Platform & Language

Listen 0%
Speed

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.

  • number is IEEE-754 double — hence 0.1 + 0.2 !== 0.3, and integer safety only up to Number.MAX_SAFE_INTEGER (2^53−1); beyond that use bigint.
  • typeof null === 'object' is a historic bug, preserved forever.
  • == does coercion (avoid); === does not (default). The coercion rules are a minefield: [] == ![] is true, null == undefined is true but null == 0 is false. Memorize the few useful ones (x == null checks 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 on null/undefined, unlike || which falls through on any falsy — a crucial distinction for 0 and "".
  • Boxing: "abc".length works because the primitive is temporarily wrapped in a String object.

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.
  • var is function-scoped and hoisted (initialized to undefined). let/const are block-scoped and hoisted but not initialized — accessing them before declaration throws (the Temporal Dead Zone). Always use const by default, let when reassigning, never var.
  • 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:

  1. newthis is the freshly created object.
  2. Explicitfn.call(obj), fn.apply(obj, args), fn.bind(obj) set this to obj.
  3. Implicitobj.method() sets this to obj. Detaching the method (const m = obj.method; m()) loses it.
  4. Defaultundefined in strict mode, globalThis otherwise.

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:

  1. Run the current task (a script, an event callback) to completion — JS is run-to-completion; it never preempts mid-task.
  2. Drain the entire microtask queue (promise .then/await continuations, queueMicrotask, MutationObserver).
  3. Render if needed.
  4. 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/.finally chain; rejections propagate down the chain.
  • async/await is syntactic sugar over promises that reads sequentially. try/catch works 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 + AbortSignal is 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-level await is 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). See 14-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 useEffect cleanup).
  • 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. Use WeakMap/WeakRef/FinalizationRegistry when 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/catch in the hot path (historically), arguments leaks, 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 & generatorsfunction*, yield, lazy sequences; the protocol behind for...of.
  • Destructuring, spread/rest, optional chaining (?.), nullish coalescing (??) — modern ergonomics.
  • Map/Set/WeakMap/WeakSet — proper collections; Map over object-as-dictionary for non-string keys and frequent add/delete.
  • Proxy/Reflect — metaprogramming; the engine behind Valtio and Vue’s reactivity (see 06).

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 this by detaching methods; using arrows where dynamic this is needed.
  • Sequential awaits that should be Promise.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

  1. Explain the event loop including microtask vs macrotask ordering. Predict the output of an interleaved setTimeout/Promise.then/await snippet.
  2. What is a closure, and give a real bug caused by one. (Stale capture; React effect with missing deps.)
  3. How is this determined? Contrast normal functions and arrows.
  4. null vs undefined; ?? vs ||; == vs ===.
  5. ESM vs CommonJS — name three concrete differences. (Static vs dynamic, live bindings vs copies, tree-shaking, async vs sync, strict-by-default.)
  6. How does prototypal inheritance work and what does class desugar to?
  7. Name three common memory leaks in a SPA and how you’d find them.
  8. How do you run two async operations in parallel and handle partial failure? (Promise.allSettled.)
  9. What is the TDZ?
  10. Why does keeping object shapes stable help V8?

Recommendations

  • const by default; === always; ??/?. liberally; never var.
  • Reach for Promise.all/allSettled reflexively 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-promise and 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

Frontend Deep-Dive Library · content is the single source of truth.