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:
- Initialize all object properties in the constructor, in a consistent order — don't add properties later.
- Don't mix types in an array or a function's parameters if it's hot.
- Avoid
deleteon hot objects (deoptimizes shape); set tonullor use aMap. - Keep functions monomorphic (same argument shapes/types).
- Avoid
argumentsleaking; 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.