JavaScript & TypeScript Runtime Deep Dive
Target audience: candidates interviewing for frontend, full-stack, or Node.js backend roles where the interviewer probes “what does the event loop actually do?”, “explain
this”, “why istypeof null === 'object'”, or “how does TypeScript narrow this union?”Scope: V8 (Chrome / Node) primarily, with mentions of SpiderMonkey (Firefox) and JSC (Safari) where they diverge. TypeScript 5.x.
JS sits in an awkward place: senior interviewers know the language is full of warts and they will use them. Memorizing trivia is necessary but not sufficient. The leverage comes from understanding the engine model and the type system’s structural reasoning.
1. V8 Internals — Ignition + TurboFan
V8 (and similar engines) compile JS through a pipeline:
- Parser → AST.
- Ignition — bytecode interpreter. Fast startup.
- TurboFan — optimizing JIT. Profiles hot code, generates speculative machine code.
- Deoptimization — when speculation breaks (a function suddenly receives a different type), TurboFan bails back to Ignition.
function add(a, b) { return a + b; }
// Called 10000x with (number, number) → TurboFan compiles it to fast int add.
add("foo", "bar"); // Type changed → deopt, recompile or bail out.
Hidden classes (shapes / maps)
V8 tracks the structural “shape” of objects internally — what fields exist in what order. Each property add changes the hidden class. Two objects with the same hidden class share a fast property layout.
function Point(x, y) { this.x = x; this.y = y; }
const a = new Point(1, 2);
const b = new Point(3, 4);
// a and b share a hidden class → fast.
a.z = 5; // a's hidden class diverges from b's → slower.
Inline caches (ICs)
Property access (obj.x) is monitored. If the hidden class is consistent, the IC fast-paths to a direct memory offset. Polymorphic ICs (multiple shapes) are slower; megamorphic (>4) drops to a hash lookup.
Interview takeaway: initialize all properties in the constructor in the same order. Don’t add/delete properties dynamically in hot code.
2. Event Loop — Tasks, Microtasks, RAF
The browser/Node runs one JS thread. Concurrency comes from yielding back to the event loop.
┌─────────────────────────┐
│ Call Stack │
└────────┬────────────────┘
│ runs to completion
┌────────▼────────────────┐
│ Microtask Queue │ ← Promises, queueMicrotask, MutationObserver
└────────┬────────────────┘
│ drained fully
┌────────▼────────────────┐
│ Task Queue │ ← setTimeout, setInterval, I/O, UI events
└─────────────────────────┘
Order of execution:
- Run the current synchronous code to completion.
- Drain the entire microtask queue.
- Pick one task from the task queue.
- Repeat from step 2.
console.log('a');
setTimeout(() => console.log('b'), 0);
Promise.resolve().then(() => console.log('c'));
console.log('d');
// a, d, c, b
Microtasks starve macrotasks if they keep enqueueing themselves:
function loop() { Promise.resolve().then(loop); }
loop(); // freezes the event loop — UI never repaints
requestAnimationFrame
Browser-only. Fires before the next paint, ~60fps. Use for animation; cheaper than setInterval(_, 16) because it’s coalesced with paint.
Node specifics
Node uses libuv. Its event loop has phases (timers, pending callbacks, poll, check, close). process.nextTick runs before microtasks (a Node-only queue with even higher priority than Promise microtasks).
process.nextTick(() => console.log('next'));
Promise.resolve().then(() => console.log('promise'));
// next, promise
3. async / await
async functions return a Promise. await desugars to .then.
async function f() {
const a = await fetch1();
const b = await fetch2(a);
return b;
}
// equivalent to:
function f() {
return fetch1().then(a => fetch2(a));
}
await x where x is not a thenable wraps it in Promise.resolve(x).
Sequential vs parallel
// Sequential (slow if independent):
const a = await op1();
const b = await op2();
// Parallel (correct when independent):
const [a, b] = await Promise.all([op1(), op2()]);
Default to Promise.all when the operations are independent — common interview ask.
Errors
try/catch works on await. An unhandled rejection in async context surfaces as unhandledrejection (browser) / process.on('unhandledRejection') (Node).
async function f() {
try {
await mayReject();
} catch (e) {
// handle
}
}
4. Promise Gotchas
-
A promise is not “the running operation” — it represents a value that will exist later. The work is already started before the Promise is constructed (in most APIs).
-
Errors thrown inside
.thencallbacks become rejections of the chain. -
Forgetting
returninside.thenbreaks chaining:p.then(x => { doSomething(x); // forgot return — next .then sees undefined }).then(use); -
Promise.allshort-circuits on first rejection. UsePromise.allSettledto wait for all and inspect. -
Unhandled rejection is now noisy in Node and the browser. Always attach a
.catchorawaitinsidetry/catch. -
Promiseis not cancelable. AbortController + AbortSignal pattern handles cancellation explicitly.
const ctrl = new AbortController();
fetch(url, { signal: ctrl.signal });
// later:
ctrl.abort();
5. Memory Model and GC
V8’s heap has generational GC:
- Young generation (Scavenger / semi-space copying): minor GC, very fast (~ms). Most objects die young.
- Old generation (Mark-sweep / mark-compact): major GC. Concurrent marking, parallel sweeping, incremental compaction.
Memory leaks in JS
- Unintentional globals — assigning to a name without
let/const(in non-strict mode) creates a global, never collected. - Closures — capturing large objects in long-lived callbacks.
- Event listeners — not removed when DOM nodes are detached.
- Timers —
setIntervalcallbacks retain captured state forever. - Detached DOM — references to removed DOM nodes from JS keep them alive.
Map/Set— keys held strongly. UseWeakMap/WeakSetfor “annotations on objects.”
Profiling
Chrome DevTools → Memory → heap snapshot, allocation timeline. Look for retained sizes and detached DOM trees.
6. Object Model — Prototypes
Every object has an internal [[Prototype]] (accessed via Object.getPrototypeOf or __proto__). Property lookup walks the prototype chain.
const a = { x: 1 };
const b = Object.create(a);
b.y = 2;
b.x; // 1 — looked up on a
Object.getPrototypeOf(b) === a; // true
prototype (the property) vs __proto__ (the link)
Foo.prototype is the object that becomes __proto__ of instances created with new Foo().
function Foo() {}
const f = new Foo();
f.__proto__ === Foo.prototype; // true
Foo.prototype.__proto__ === Object.prototype;
class
class is sugar over prototypes. The methods live on Foo.prototype.
class Foo {
greet() { return 'hi'; }
}
typeof Foo.prototype.greet; // 'function'
Object.create(null)
A “dictionary object” with no prototype — no inherited properties from Object.prototype. Useful as a hash map.
7. this Binding
JS binds this at call site, not at definition. Five rules in priority order:
new:this= new instance.call/apply/bind: explicit binding wins.- Method call (
obj.f()):this=obj. - Plain call (
f()):this=undefinedin strict mode, global object otherwise. - Arrow functions: no own
this— inherit from surrounding lexical scope.
const obj = { x: 1, f() { return this.x; } };
const g = obj.f;
obj.f(); // 1
g(); // undefined (strict) — `this` is global / undefined
class C {
val = 42;
arrow = () => this.val; // bound to instance
method() { return this.val; }
}
const c = new C();
const a = c.arrow;
const m = c.method;
a(); // 42 — arrow captured `this`
m(); // TypeError — method lost `this`
8. Closures, var / let / const, Scope
A closure is a function plus the lexical environment it was created in.
function counter() {
let n = 0;
return () => ++n;
}
const c = counter();
c(); c(); c(); // 1, 2, 3
var (function-scoped, hoisted)
console.log(x); // undefined (hoisted, not initialized)
var x = 1;
let / const (block-scoped, TDZ)
console.log(y); // ReferenceError — TDZ
let y = 1;
The “Temporal Dead Zone” is the period between block entry and the let/const declaration. Accessing the binding in TDZ throws.
Loop var capture
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);
// 3 3 3 — single `i`, all callbacks share it
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);
// 0 1 2 — fresh `i` per iteration
This is the classic JS interview question. Use let.
9. Equality
===strict — same type and value, withNaN !== NaNand+0 === -0.==loose — type coercion. Don’t use it except forx == null(matches bothnullandundefined).Object.is(a, b)— like===butObject.is(NaN, NaN) === trueandObject.is(+0, -0) === false.
NaN === NaN; // false
Object.is(NaN, NaN); // true
1 == '1'; // true (coercion)
[] == false; // true (!)
[] == ![]; // true (!)
The == rules are an interview trap. Memorize only the null/undefined exception; use === everywhere else.
10. Top Gotchas
typeof null === 'object'
A historical bug, never fixed for compatibility. Test for null with === null.
parseInt radix
parseInt('010'); // 10 in modern engines (used to be 8)
parseInt('010', 10); // always 10 — pass the radix.
Always pass 10. ESLint enforces this.
Floating-point
0.1 + 0.2; // 0.30000000000000004
0.1 + 0.2 === 0.3; // false
Use Number.EPSILON tolerance, or bigint for exact integer arithmetic.
Array coercion
[] + []; // ''
[] + {}; // '[object Object]'
{} + []; // 0 (in some contexts — `{}` parsed as block)
Don’t + non-numbers. Use template literals or explicit String(x).
== and falsy
0 == ''; // true
0 == '0'; // true
'' == '0'; // false
This is why === exists.
for...in vs for...of
for (const k in obj)iterates enumerable string-keyed properties (includes inherited!).for (const v of iterable)iterates iterable’s values.
const arr = [1, 2, 3];
for (const i in arr) console.log(i); // '0' '1' '2' — strings, indexes
for (const v of arr) console.log(v); // 1 2 3
Don’t use for...in on arrays.
delete on array
delete arr[i] leaves a hole (sparse array), doesn’t shorten. Use splice.
11. Map vs Object
Object | Map | |
|---|---|---|
| Keys | Strings & symbols | Anything |
| Iteration | Object.keys/entries, no order guarantee for non-int | Insertion order |
| Size | Object.keys(o).length (O(N)) | m.size (O(1)) |
| Inheritance pollution | Yes (__proto__, toString…) | No |
| JSON | Yes | No (need conversion) |
Use Map when:
- Keys are dynamic strings (esp. user input).
- You need any-typed keys.
- Insertion order matters.
- You add/remove keys frequently.
Use Object when:
- Keys are known compile-time / config-shaped.
- You’ll JSON-serialize.
- You’re using TypeScript’s structural types.
12. Set, WeakMap, WeakSet
Set— collection of unique values; insertion order.WeakMap— keys are objects, weakly held. If the key is GC’d, the entry disappears. Not iterable. Use for “annotations on objects.”WeakSet— set of weakly-held objects. Use for “have I seen this object?” without preventing GC.
const tags = new WeakMap();
function tag(node, value) { tags.set(node, value); }
// when `node` is GC'd, the tag is gone too.
Practical use: caches keyed on DOM nodes, private state on objects, libraries that observe but don’t own.
WeakRef (newer)
new WeakRef(obj) lets you hold a weak reference and dereference it (.deref()) later, getting the object or undefined if collected. Niche — you probably don’t need it.
13. TypeScript — Structural Typing & Generics
TypeScript types are structural. If two types have the same shape, they’re compatible.
interface Named { name: string }
const u: Named = { name: 'a', age: 30 }; // OK — extra props allowed in this position
function greet(p: Named) { return p.name; }
greet({ name: 'a', extra: 1 } as any);
Generics
function identity<T>(x: T): T { return x; }
identity<number>(42);
identity('hi'); // T inferred as string
Constraints
function len<T extends { length: number }>(x: T): number { return x.length; }
Conditional types
type IsString<T> = T extends string ? true : false;
type A = IsString<'hi'>; // true
type B = IsString<42>; // false
Mapped types
type Partial<T> = { [K in keyof T]?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };
Utility types
Partial<T>, Required<T>, Pick<T, K>, Omit<T, K>, Record<K, V>, ReturnType<F>, Parameters<F>, Awaited<T>. Memorize the names; they come up.
14. TS Narrowing
The control-flow analyzer narrows union types based on runtime checks.
function f(x: string | number) {
if (typeof x === 'string') {
x.toUpperCase(); // narrowed to string
} else {
x.toFixed(2); // narrowed to number
}
}
Narrowing operators
-
typeof→"string","number","boolean","undefined","object","function","symbol","bigint". -
instanceof→ for class instances. -
inoperator:if ('foo' in obj). -
Equality:
if (x === null),if (x === undefined). -
Discriminated unions:
type Result = { ok: true; value: string } | { ok: false; error: Error }; function f(r: Result) { if (r.ok) r.value; // OK else r.error; // OK } -
User-defined type guards:
function isString(x: unknown): x is string { return typeof x === 'string'; } -
Assertion functions:
function assertNumber(x: unknown): asserts x is number { if (typeof x !== 'number') throw new Error('not a number'); }
Exhaustiveness with never
type Shape = { kind: 'circle' } | { kind: 'square' };
function area(s: Shape): number {
switch (s.kind) {
case 'circle': return ...;
case 'square': return ...;
default: const _: never = s; throw new Error('unreachable');
}
}
The never assignment fails to compile if a new variant is added — catches missing cases.
15. Performance Tips
- Stable hidden classes — set all properties in the constructor in the same order. Don’t add later.
- Avoid
deleteon hot objects — it transitions to dictionary mode. - Monomorphic functions — call them with the same shapes. Polymorphic = slower.
- Typed arrays for numeric work —
Float64Array,Int32Array. Pre-allocated, contiguous, no boxing. - Avoid
argumentsin hot code; use...rest.argumentsdefeats some optimizations. foroverforEachin hot loops — slightly faster, no callback overhead. Less true with modern engines but still measurable on tight loops.- Pre-compile regexes — declare at module scope, not inside functions.
- Avoid leaking with
try/catchin hot functions on old V8 (pre-2017). Modern V8 handles it; not a real concern anymore. - Profile before optimizing. Chrome DevTools Performance tab; Node
--profandclinic.js. - Reduce object churn — V8 likes long-lived monomorphic objects.
// Bad — creates new shapes per call:
function pt() { return { x: 1, y: 2 }; }
// Better — a class V8 can specialize:
class Pt { constructor(x, y) { this.x = x; this.y = y; } }
16. Node vs Browser
| Node | Browser | |
|---|---|---|
| Globals | process, Buffer, __dirname, global | window, document, navigator |
| Modules | CommonJS (require) + ESM | ESM + bundlers |
| I/O | libuv: fs, net, dns, child_process | fetch, Web APIs |
| DOM | None (use jsdom if needed) | Yes |
| Threads | worker_threads, cluster | Worker, SharedArrayBuffer |
libuv thread pool
Node uses a thread pool (default 4) for fs, dns.lookup, crypto.pbkdf2, etc. — anything that can’t be epoll-ed.
UV_THREADPOOL_SIZE=16 node app.js
Network I/O is not on the thread pool; it’s on the event loop using async syscalls.
worker_threads vs cluster
worker_threads— separate JS thread, separate event loop, can share memory viaSharedArrayBuffer. Use for CPU-bound work.cluster— multiple processes, no shared memory, IPC via channels. Use for scaling HTTP servers across cores.
process vs globalThis
globalThis (ES2020) is the universal global object — works in browser, Node, workers.
17. What To Memorize Cold
- V8 pipeline: Ignition (bytecode) → TurboFan (optimizing JIT). Hidden classes + ICs drive speed. Property order matters.
- Event loop: sync runs to completion → drain microtasks → one task → repeat. Microtasks include Promises, queueMicrotask.
- Node nextTick > Promise microtask > timers/IO.
async/awaitdesugars to Promises. UsePromise.allfor independent work.- Promises: not cancelable, errors → rejection, must catch. AbortController for cancellation.
- GC: generational. Leaks: globals, closures, listeners, timers, detached DOM. WeakMap/WeakSet for object-keyed metadata.
- Prototype chain:
__proto__link,prototypeproperty on functions/classes.classis sugar. thisrules: new > call/apply/bind > method > default. Arrow inherits lexical.varhoisted/function-scoped,let/constblock-scoped + TDZ. Loop var capture: uselet.- Equality:
===always;Object.isfor NaN/±0;==only forx == null. - Top traps:
typeof nullis"object",parseIntradix, FP math,for...invsfor...of,deleteon array. MapvsObject: Map for dynamic keys, any types, ordered, fastsize.WeakMapfor object-keyed weak metadata.- TS structural typing. Utility types: Partial/Required/Pick/Omit/Record/ReturnType. Conditional + mapped types.
- TS narrowing:
typeof,instanceof,in, discriminated unions, type guards, assertion functions,neverfor exhaustiveness. - Perf: stable hidden classes, monomorphic call sites, typed arrays for numeric, pre-compile regex, profile.
- Node: libuv thread pool for fs/dns/crypto.
worker_threads(CPU) vscluster(HTTP scale).
JS is forgiving until it isn’t. The interviewer will test the spots where it isn’t. Fluency on the event loop, this, equality, and TypeScript narrowing usually decides senior-level outcomes.