Interview Prep: Senior Level
Advanced JavaScript interview questions for 4+ year experience — engine internals, design patterns, architecture, and tricky outputs
Last updated on
These questions test deep engine knowledge, architectural thinking, and real-world problem-solving. Expect these at senior (4+ YOE) and lead-level interviews.
Engine Internals
1. Explain the JavaScript execution context in detail.
Every function call creates an Execution Context with two phases:
Creation Phase:
- Create the Variable Environment (hoist
var, functions) - Create the Scope Chain (lexical environment references)
- Determine
this
Execution Phase:
- Execute code line by line
- Assign values to variables
The Call Stack manages contexts — push on call, pop on return. Stack overflow happens when recursion exceeds the stack limit (~10,000-15,000 frames).
📖 Deep dive: Execution Context
2. How does the V8 engine optimize JavaScript?
Source Code → Parser → AST → Ignition (Interpreter, Bytecode)
↓
TurboFan (JIT Compiler, Optimized Machine Code)
↓
Deoptimization (if assumptions break)Key optimizations:
- Inline Caching: Remembers object shapes for fast property access
- Hidden Classes: Objects with same property order share a class
- Deoptimization: Falls back to bytecode when assumptions break (e.g., changing property types)
Why this matters: Consistent object shapes, avoiding delete, and keeping functions monomorphic makes your code faster.
📖 Deep dive: Memory & Performance
3. What is the Temporal Dead Zone (TDZ) and why does it exist?
The TDZ is the region between the start of a scope and the point where a let/const is declared. Accessing the variable in this zone throws ReferenceError.
{
// TDZ starts here for `x`
console.log(x); // ❌ ReferenceError
let x = 10; // TDZ ends here
}Why it exists: Prevents bugs from using variables before they're initialized. var allows this and it causes subtle bugs.
📖 Deep dive: Hoisting
4. Explain stale closures and how to avoid them.
A closure captures variables by reference, not by value. If the variable changes, the closure sees the new value:
function createCallbacks() {
const callbacks = [];
for (var i = 0; i < 3; i++) {
callbacks.push(() => console.log(i));
}
return callbacks;
}
createCallbacks().forEach(cb => cb()); // 3, 3, 3In React, stale closures happen when a useEffect captures old state:
// ❌ Stale: `count` is captured once
useEffect(() => {
setInterval(() => console.log(count), 1000);
}, []);
// ✅ Fix: use ref or functional updater
setCount(prev => prev + 1);📖 Deep dive: Closures
Memory & Performance
5. What causes memory leaks in JavaScript?
Common causes:
- Accidental globals:
function() { leak = "oops"; }(missinglet/const) - Forgotten timers:
setIntervalthat's never cleared - Detached DOM nodes: Removed from DOM but referenced in JS
- Closures holding large objects: Inner function keeps outer scope alive
- Event listeners: Added but never removed
// ❌ Leak: closure retains `hugeData`
function setup() {
const hugeData = new Array(1e6);
return () => hugeData.length; // hugeData never GC'd
}📖 Deep dive: Memory & Performance
6. Explain WeakMap and WeakRef. When would you use them?
WeakMap: Keys must be objects and are held weakly — if nothing else references the key, it gets garbage collected along with its value.
const cache = new WeakMap();
let obj = { data: "big" };
cache.set(obj, "computed result");
obj = null; // obj is GC'd, cache entry disappears automaticallyWeakRef: A weak reference to an object. Doesn't prevent garbage collection.
Use cases: Caching, memoization without memory leaks, metadata on DOM elements.
📖 Deep dive: ES6+ Features
7. What is requestAnimationFrame and why use it over setInterval for animations?
rAF syncs with the browser's repaint cycle (60fps = every ~16ms):
function animate() {
element.style.transform = `translateX(${x++}px)`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);vs setInterval: rAF pauses in background tabs (saves CPU), syncs with display refresh rate, doesn't cause jank.
📖 Deep dive: Browser APIs
Design Patterns & Architecture
8. Explain the Module Pattern and why it matters.
Encapsulates private state using closures:
const UserService = (() => {
let users = []; // private
return {
add(user) { users.push(user); },
getAll() { return [...users]; },
count() { return users.length; }
};
})();With ES modules, the module pattern is less necessary, but understanding it shows deep closure mastery.
📖 Deep dive: Design Patterns
9. Implement a pub/sub (Observer) pattern.
class EventBus {
#events = {};
on(event, callback) {
(this.#events[event] ??= []).push(callback);
return () => this.off(event, callback); // unsubscribe
}
off(event, callback) {
this.#events[event] = this.#events[event]?.filter(cb => cb !== callback);
}
emit(event, ...args) {
this.#events[event]?.forEach(cb => cb(...args));
}
}📖 Deep dive: Design Patterns
10. What is the Proxy object? Give a real use case.
Proxy intercepts fundamental operations on objects:
const validator = new Proxy({}, {
set(target, prop, value) {
if (prop === "age" && typeof value !== "number") {
throw new TypeError("Age must be a number");
}
target[prop] = value;
return true;
}
});
validator.age = 25; // ✅
validator.age = "old"; // ❌ TypeErrorReal use cases: Vue 3 reactivity, validation, logging, default values, access control.
📖 Deep dive: ES6+ Features
Polyfill & Implementation
11. Implement Promise.all from scratch.
function promiseAll(promises) {
return new Promise((resolve, reject) => {
const results = [];
let completed = 0;
if (promises.length === 0) return resolve([]);
promises.forEach((p, i) => {
Promise.resolve(p).then(value => {
results[i] = value;
if (++completed === promises.length) resolve(results);
}, reject);
});
});
}📖 More implementations: Polyfills
12. Implement Function.prototype.bind.
Function.prototype.myBind = function (context, ...boundArgs) {
const fn = this;
return function (...callArgs) {
return fn.apply(context, [...boundArgs, ...callArgs]);
};
};📖 More implementations: Polyfills
13. Implement an LRU Cache.
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return -1;
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value); // move to end
return value;
}
put(key, value) {
if (this.cache.has(key)) this.cache.delete(key);
this.cache.set(key, value);
if (this.cache.size > this.capacity) {
this.cache.delete(this.cache.keys().next().value);
}
}
}📖 More challenges: Machine Coding
Advanced Concepts
14. What are generators and when would you use them?
Functions that can be paused and resumed with yield:
function* idGenerator() {
let id = 1;
while (true) yield id++;
}
const gen = idGenerator();
gen.next().value; // 1
gen.next().value; // 2Use cases: Infinite sequences, lazy evaluation, Redux-Saga, custom iterables, paginated API fetching.
📖 Deep dive: Iterators & Generators
15. Explain Symbol and its use cases.
Symbols are unique, immutable identifiers:
const id = Symbol("id");
const user = { [id]: 123, name: "Shiva" };
user[id]; // 123
Object.keys(user); // ["name"] — symbol keys are hiddenUse cases: Private-ish properties, custom iterators (Symbol.iterator), avoiding name collisions in libraries.
📖 Deep dive: Data Types
16. What is Object.create(null) and why use it?
Creates an object with no prototype (no .toString, .hasOwnProperty, etc.):
const dict = Object.create(null);
dict.constructor; // undefined — truly cleanUse case: Pure dictionaries/maps where you don't want prototype pollution. Used internally by many frameworks.
Guess the Output (Tricky)
Q1
const a = {};
const b = { key: "b" };
const c = { key: "c" };
a[b] = 123;
a[c] = 456;
console.log(a[b]);Output: 456
Objects as keys get stringified to "[object Object]". Both b and c become the same key, so c overwrites b.
Q2
function Foo() {
return this;
}
console.log(new Foo() === new Foo());Output: false — new creates a fresh object each time.
Q3
const promise = new Promise((resolve) => {
console.log(1);
resolve(2);
console.log(3);
});
promise.then(console.log);
console.log(4);Output: 1, 3, 4, 2
The Promise executor runs synchronously (1, 3). resolve(2) schedules .then as a microtask. 4 runs. Then microtask 2.
Q4
var x = 1;
{
var x = 2;
}
console.log(x);
let y = 1;
{
let y = 2;
}
console.log(y);Output: 2, 1 — var ignores block scope. let respects it.