Topics in this subject
JavaScript 4 min read Updated 5 Aug 2026

Advanced JavaScript

Execution context & the call stack, Memory, garbage collection, weak references, Symbols & well-known symbols, Iterators & generators, Tail calls

Execution context & the call stack#

Each function call pushes an execution context (with its variable environment, scope chain, and this) onto the call stack. Returning pops it. A stack overflow is too many nested contexts (usually runaway recursion).

graph TD
    subgraph "Call Stack (top runs)"
    c["multiply() context"]
    b["square() context"]
    a["main() / global context"]
    end
    c --> b --> a

Memory, garbage collection, weak references#

JS uses a mark-and-sweep GC: starting from roots (global, stack), it marks everything reachable; the unmarked is swept. You can't force GC. Leaks happen when you unintentionally keep references: forgotten timers, detached DOM nodes held in a variable, growing caches, closures over big data, and listeners never removed.

WeakMap/WeakSet/WeakRef/FinalizationRegistry hold objects weakly so they don't block collection — use for caches and metadata keyed by object.

Symbols & well-known symbols#

const id = Symbol("id");             // unique, non-colliding key
obj[id] = 1;                          // hidden from normal enumeration
Symbol.for("x") === Symbol.for("x"); // true — global registry
// well-known symbols customize built-in behaviour:
class Range {
  constructor(a,b){this.a=a;this.b=b;}
  *[Symbol.iterator]() { for (let i=this.a;i<=this.b;i++) yield i; }
}
[...new Range(1,3)];                  // [1,2,3]

Iterators & generators#

// iterator protocol: an object with next() → { value, done }
function makeIter(arr) {
  let i = 0;
  return { next: () => i < arr.length ? {value:arr[i++],done:false} : {value:undefined,done:true},
           [Symbol.iterator]() { return this; } };
}

// generators produce iterators effortlessly and can pause/resume
function* fib() { let [a,b]=[0,1]; while(true){ yield a; [a,b]=[b,a+b]; } }
const g = fib(); g.next().value; // 0 → 1 → 1 → 2 ...

// generators can receive values and delegate
function* outer(){ yield* [1,2]; const x = yield 3; return x; }

Async generators & iterators:

async function* pages(url) {
  let next = url;
  while (next) {
    const res = await fetch(next); const data = await res.json();
    yield data.items;
    next = data.nextUrl;
  }
}
for await (const items of pages("/api")) process(items);

Tail calls#

The spec defines proper tail calls (a call in tail position reuses the stack frame), but V8 does not implement it. So don't rely on TCO in Node/Chrome; convert deep recursion to loops.

Decorators (Stage 3 proposal) 🟡#

function logged(value, ctx) {
  return function (...args) { console.log(`call ${ctx.name}`); return value.call(this, ...args); };
}
class Api { @logged fetchData() {} }

Available today via TypeScript/Babel; native support is arriving. Used for cross-cutting concerns (logging, memo, validation, DI).

Engine performance internals 🎯#

  • Hidden classes (shapes/maps): V8 assigns objects an internal "shape" describing their properties. Objects created with the same properties in the same order share a shape → fast property access via offsets.
  • Inline caches (ICs): V8 caches where a property lives for a given shape at each access site. Consistent shapes keep the IC "monomorphic" (fastest); mixed shapes make it "polymorphic" or "megamorphic" (slow).
  • Deoptimization: if your assumptions break (a function suddenly gets a different type, an object changes shape mid-loop, you add properties dynamically), V8 throws away optimized code and falls back to bytecode.

Practical rules for fast code:

  1. Initialize all object properties in the constructor, in a consistent order — don't add properties later.
  2. Don't mix types in an array or a function's parameters if it's hot.
  3. Avoid delete on hot objects (deoptimizes shape); set to null or use a Map.
  4. Keep functions monomorphic (same argument shapes/types).
  5. Avoid arguments leaking; use rest params.

Interview Q&A#

Q1. Explain mark-and-sweep GC and name three common leak sources. GC starts from roots (globals, the stack), marks everything reachable, then sweeps the unmarked memory; you can't force it. Common leaks: forgotten timers/setInterval, detached DOM nodes still referenced in JS, and ever-growing caches or closures over large data (also listeners never removed).

Q2. What are hidden classes and inline caches, and how do they affect performance? V8 assigns each object a hidden class (shape) describing its properties; objects built with the same properties in the same order share a shape, enabling fast offset-based access. Inline caches remember where a property lives per shape at each access site — consistent shapes keep them monomorphic (fast), while mixed shapes go polymorphic/megamorphic and slow down.

Q3. Difference between an iterator and a generator; write a generator for Fibonacci. An iterator is any object with a next() returning {value, done}; a generator is a function (function*) that produces an iterator automatically and can pause/resume with yield.

function* fib() { let [a, b] = [0, 1]; while (true) { yield a; [a, b] = [b, a + b]; } }

Q4. Does JS optimize tail calls? What are the implications? The spec defines proper tail calls, but V8 (Node/Chrome) does not implement them. So deep recursion will still overflow the stack — convert hot/deep recursion to iterative loops rather than relying on TCO.

Q5. What are Symbols for? Give a well-known symbol example. Symbols are unique, non-colliding keys ideal for hidden or metadata properties that won't clash with string keys. Well-known symbols customize built-in behaviour — e.g. Symbol.iterator makes an object iterable so it works with for...of and spread.