Free lesson · JavaScript Basic to Advance
Interview Question Bank: Full Answers Across the Course
The Final Reference
This Topic is the comprehensive interview question bank. Topic 14.2 was a navigation map — questions paired with one-line answer skeletons and pointers to source Topics. Topic 15.6 is different: actual interview questions with the full answers you'd deliver out loud, organized by topic cluster, with the capstone project's code as a concrete reference when relevant.
// How to use this Topic:
//
// 1. Pick a cluster (Runtime, Closures, Async, etc.)
// 2. Cover the answer — read only the question
// 3. Try to answer out loud, as if in an interview
// 4. Compare your answer to the model
// 5. Note gaps to study
//
// The questions are real ones from FAANG interviews. The answers are
// the full versions a senior candidate would give.
This is the largest Topic in the course. ~3,500 words of pure interview-prep material. Treat it as a workbook you return to often — not as something to read once.
Olena framing this Topic. The answers themselves draw on whichever founder's voice owned that material across the course — Nikhil for runtime and closures, Brandon for OO, Emeka for async, me for modern syntax and modules.
The capstone project from Topics 15.1-15.5 is referenced throughout. When an answer benefits from a concrete code example, we point to the actual code in the project.
Interactive animation: Cover, attempt, reveal: the bank's method, run on four core questions from == vs === to arrow vs function.
Cluster 1: Runtime Model
Q: Explain the event loop.
Answer. JavaScript is single-threaded — one call stack, one thing executing at a time. The event loop is the mechanism that pulls async work back to the main thread when it's ready.
Two queues are involved. The microtask queue holds Promise callbacks (.then, .catch, .finally), await continuations, and queueMicrotask callbacks. The task queue holds setTimeout, setInterval, I/O callbacks, and UI events.
The event loop's rule: when the call stack is empty, drain the entire microtask queue. Then run one task from the task queue. Then drain microtasks again. Repeat.
This is why Promise.resolve().then(fn) runs before setTimeout(fn, 0) even though both are scheduled at the "next moment" — microtasks have priority over tasks.
In our capstone project, this affects the order of state updates and re-renders. When store.update() triggers subscribers, those subscribers run synchronously. If a subscriber schedules a Promise via .then, that runs before any setTimeout-based work the page might have pending.
Pointer: Topics 9.1, 9.2, 9.11.
Q: Predict the output of this code:
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
Answer. Output: 1, 4, 3, 2.
Synchronous code runs first: 1, then 4. The setTimeout schedules a task. The Promise.resolve().then schedules a microtask.
After synchronous code finishes, the event loop drains the microtask queue first. That's 3. Then it pulls one task from the task queue. That's 2.
The key insight: microtasks run before any task, even when the task has a 0ms delay.
Pointer: Topic 9.2, 9.11.
Q: What's the difference between value types and reference types?
Answer. Primitives — numbers, strings, booleans, null, undefined, symbols, bigint — are value types. They're copied when passed or assigned. Modifying one variable doesn't affect another.
Objects, arrays, and functions are reference types. They're not copied — variables hold references to the same underlying object. Modifying through one reference is visible through any other reference to the same object.
let a = 5; let b = a; b = 10;
console.log(a); // 5 — primitives are copied
let x = { count: 0 }; let y = x; y.count = 10;
console.log(x.count); // 10 — objects are shared references
This matters when passing objects to functions. The function gets a reference, not a copy. Modifying properties of the argument affects the caller's object.
In our capstone, this is why the Store returns { ...this.#state } from getState() — a shallow copy. Without it, callers could mutate state directly, bypassing the update mechanism and breaking the subscriber notifications.
Pointer: Topic 7.1, 7.8.
Cluster 2: Closures and Scope
Q: What is a closure?
Answer. A closure is a function bundled with references to the variables in its enclosing scope. When the outer function returns, the inner function keeps access to those variables — they don't get garbage collected because the closure still references them.
This enables several patterns:
- Private state: variables only accessible to specific functions
- Memoization: caching results in the closure's scope
- Currying: partial application of arguments
- Factory functions: creating functions with captured configuration
Our capstone's cache in data/cache.js is exactly this. The entries Map is private — only the methods returned by createCache can access it. Calling cache.get() works; trying to access cache.entries returns undefined because there's no exposed property. The closure is what enforces the privacy.
Pointer: Topics 6.3, 6.4, 6.7.
Q: Explain why this prints 3, 3, 3 instead of 0, 1, 2 — and how to fix it.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
Answer. var is function-scoped, not block-scoped. The three setTimeout callbacks all close over the same i — there's only one. By the time the timeouts fire (100ms later), the loop has finished and i is 3.
Two fixes:
Use let. let is block-scoped, so each loop iteration has its own i. Each callback closes over its own iteration's variable.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 0, 1, 2
Use an IIFE to create a new scope per iteration. The pre-ES6 fix.
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
let is the modern fix. The IIFE pattern is useful to recognize in older code.
This is one of the most-asked closure questions in JavaScript interviews. The pattern of "closures sharing scope unexpectedly" comes up in real code too — event handlers in loops, async work in iterators, etc.
Pointer: Topic 6.6, 6.7.
Q: Implement a function createCounter() that returns an object with increment, decrement, and getCount methods. The count should be truly private.
Answer. Two valid approaches.
Closure approach (what we used in our cache):
function createCounter(initial = 0) {
let count = initial;
return {
increment() { count++; return count; },
decrement() { count--; return count; },
getCount() { return count; }
};
}
const c = createCounter(10);
console.log(c.increment()); // 11
console.log(c.increment()); // 12
console.log(c.getCount()); // 12
// c.count is undefined — no way to access from outside
Class with private field approach:
class Counter {
#count;
constructor(initial = 0) { this.#count = initial; }
increment() { this.#count++; return this.#count; }
decrement() { this.#count--; return this.#count; }
getCount() { return this.#count; }
}
const c2 = new Counter(10);
console.log(c2.increment()); // 11
// c2.#count is a syntax error from outside
Both produce real privacy. The closure approach was the only option before ES2022. Modern class-based code uses private fields with #. For simple state holders like a counter, the closure version is often cleaner — no this binding considerations, no class boilerplate.
Pointer: Topics 6.4, 8.5.
Cluster 3: Objects, Prototypes, Classes
Interactive animation: Bank two, closures to classes: the opening sentence for closures, this, the prototype chain and class as sugar.
Q: Are classes real or syntactic sugar in JavaScript?
Answer. Syntactic sugar over the prototype mechanism.
class User { greet() {} } creates:
- A constructor function
User - A prototype object
User.prototypewithgreeton it - Instances created with
new User()linked toUser.prototypevia their[[Prototype]]
You can verify: typeof User === "function". User.prototype.greet exists. (new User()).greet === User.prototype.greet is true (the instance and the prototype see the same function — it's not duplicated per instance).
The mechanism is identical to the older constructor-function pattern:
function User() {}
User.prototype.greet = function() {};
The class syntax is cleaner and standardizes inheritance patterns that used to vary across libraries. But underneath, it's prototypes.
Knowing this matters in interviews because edge cases — extending built-in classes, working with instanceof, debugging unexpected behavior — are best understood at the prototype level.
Pointer: Topic 8.2, 8.7.
Q: How does this work in JavaScript?
Answer. Four rules, based on how the function is called:
- Method call (
obj.method()) —thisisobj - Plain function call (
fn()) —thisisundefinedin strict mode, the global object otherwise - Constructor call (
new Fn()) —thisis the newly created instance - Explicit binding (
fn.call(obj),fn.apply(obj),fn.bind(obj)) —thisis the first argument
Arrow functions ignore all four rules. They inherit this from the surrounding lexical scope.
The most common bug: a method loses its this when detached. const fn = obj.method; fn() calls fn as a plain function, so this is undefined. To preserve this, bind explicitly: const fn = obj.method.bind(obj).
In our capstone, the Store class uses this implicitly throughout its methods. When store.update() is called, this is store because of rule 1. If we ever pass store.update as a callback without binding, it would break.
Pointer: Topic 8.1, 8.7.
Q: Implement new from scratch.
Answer. Four steps:
function myNew(constructor, ...args) {
// 1. Create a new object with the constructor's prototype
const obj = Object.create(constructor.prototype);
// 2. Call the constructor with 'this' bound to the new object
const result = constructor.apply(obj, args);
// 3. If the constructor returns an object, return that; otherwise the new object
return (typeof result === "object" && result !== null) ? result : obj;
}
function User(name) {
this.name = name;
}
User.prototype.greet = function() {
return `Hi, ${this.name}`;
};
const u = myNew(User, "Olena");
console.log(u.name); // "Olena"
console.log(u.greet()); // "Hi, Olena"
console.log(u instanceof User); // true
The four steps mirror what new does internally:
- Create empty object linked to the constructor's prototype
- Run the constructor function with
thisset to the new object - If the constructor returns an object, use that; otherwise use the new object
- Return the result
Step 3 is the gotcha. Most constructors return undefined implicitly. But constructors can explicitly return objects, and new returns those instead of this. Forgetting this is a common mistake in implement-new questions.
Pointer: Topic 7.8.
Q: When should you use inheritance vs composition?
Answer. Inheritance for clear "is-a" relationships. Composition for behavior reuse and "has-a" relationships.
Inheritance is appropriate when:
- The relationship genuinely is "X is a kind of Y" (Dog is an Animal)
- The hierarchy is stable and well-understood
- You'll use
instanceofpolymorphism (treating all subtypes as the supertype)
Composition is preferred when:
- The relationship is "X contains/uses Y" rather than "X is a Y"
- You want flexibility to swap implementations
- You want to combine behaviors orthogonally (multiple mixins)
Modern OO design favors composition over inheritance. Deep inheritance hierarchies (3+ levels) often signal that composition would be cleaner.
In our capstone, we use composition: the Store doesn't extend anything. The Api doesn't extend the Cache — it composes with it (the Api gets a cache injected and uses it). If we needed multiple cache strategies, we'd swap the cache implementation without touching the Api code.
Pointer: Topic 8.6, 8.7.
Cluster 4: Async
Interactive animation: Async in four opening sentences: the event loop, what a Promise is, whether await blocks, and all vs allSettled.
Q: What is a Promise?
Answer. A Promise is an object representing a future value — a placeholder for an async operation's eventual result.
Three states:
- Pending — operation not yet complete
- Fulfilled — operation succeeded, Promise has a value
- Rejected — operation failed, Promise has a reason (usually an Error)
Once a Promise settles (becomes fulfilled or rejected), its state can't change. It can only transition once, in one direction. This is what makes Promises trustworthy in a way callbacks aren't — you can't accidentally fire a Promise's success callback twice.
Promises also flatten: returning a Promise from a .then handler causes the next .then to wait for that inner Promise. This is what enables chaining.
Pointer: Topic 9.4, 9.12.
Q: Compare .then/.catch with async/await.
Answer. Same mechanism, different syntax. async/await is syntactic sugar over Promises.
An async function always returns a Promise. Whatever you return from inside becomes the Promise's fulfilled value. If you throw, the Promise rejects.
await pauses execution until the awaited Promise settles. The value of await someP is the resolved value of someP. If someP rejects, await throws — which can be caught by try/catch.
// These two are equivalent:
function f1() {
return getData().then(data => data.processed);
}
async function f2() {
const data = await getData();
return data.processed;
}
The async/await version reads top-to-bottom and uses regular try/catch for errors. The .then version chains callbacks. For sequential async code, async/await is almost always clearer. For combining multiple Promises in flight, Promise.all (and friends) is often more direct.
In our capstone, fetchWithRetry uses both — the outer loop uses async/await for sequential clarity, and the retry's wait uses new Promise(resolve => setTimeout(resolve, ...)) because that's how you await a delay.
Pointer: Topic 9.6.
Q: Implement Promise.all from scratch.
Answer.
function myAll(promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve([]);
return;
}
const results = new Array(promises.length);
let completedCount = 0;
promises.forEach((p, index) => {
Promise.resolve(p).then(
value => {
results[index] = value;
completedCount++;
if (completedCount === promises.length) resolve(results);
},
reject // first rejection rejects the outer Promise
);
});
});
}
myAll([
Promise.resolve("a"),
new Promise(r => setTimeout(() => r("b"), 50)),
Promise.resolve("c")
]).then(r => console.log(r)); // ["a", "b", "c"]
Three things to get right:
- Preserve order. Results array indexed by original position, not by resolution order.
- Handle empty input.
Promise.all([])should resolve immediately with[], not hang. - Fail fast. First rejection rejects the outer Promise. Subsequent Promise settles don't matter (the outer is already settled — additional resolve/reject calls are no-ops).
Common follow-ups: implement Promise.race (resolve or reject with whichever settles first), Promise.allSettled (always resolves, with a status report per Promise), Promise.any (resolves with first success, rejects only if all fail).
Pointer: Topic 9.8, 9.12.
Q: How does AbortController work?
Answer. AbortController provides a standard mechanism for cancelling in-flight async operations.
You create a controller, get its signal, and pass that signal to the operation (like fetch). Later, calling controller.abort() cancels the operation — for fetch, the Promise rejects with an AbortError.
const controller = new AbortController();
fetch("/api/data", { signal: controller.signal })
.then(r => r.json())
.catch(err => {
if (err.name === "AbortError") console.log("Cancelled");
});
setTimeout(() => controller.abort(), 1000);
In our capstone, the Api class uses this. Each fetch call creates a new controller; calling api.cancel() (or starting a new fetch) aborts the previous one. This is essential for search-as-you-type — without cancellation, a flood of in-flight requests could deliver results in the wrong order.
The same signal pattern works with addEventListener for removing many listeners at once — pass the signal in the listener options.
Pointer: Topic 9.7, 12.5.
Q: Implement a retry pattern with exponential backoff.
Answer.
async function retry(fn, { maxAttempts = 3, baseDelay = 100 } = {}) {
let lastError;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err;
if (attempt < maxAttempts - 1) {
const delay = baseDelay * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw new Error(`Failed after ${maxAttempts} attempts: ${lastError.message}`);
}
Key elements:
- Loop with try/catch
- Store the last error in case all attempts fail
- Wait between attempts, with exponentially increasing delay (100ms, 200ms, 400ms, ...)
- After max attempts, throw a wrapper error
Real production retry adds: jitter (random variation in delay to avoid thundering herd), retryable-error classification (don't retry 4xx, only 5xx), and circuit breakers (stop retrying entirely after sustained failures).
In our capstone's fetchWithRetry, we use this pattern — with one important refinement: we don't retry AbortError. Cancellations are intentional, not failures.
Pointer: Topic 9.7, 9.12.
Cluster 5: Modern JavaScript
Q: What's the difference between == and ===?
Answer. == does type coercion before comparing. === requires both type and value to match.
== follows complex coercion rules: string vs number coerces the string to a number; boolean coerces to number; null and undefined are equal to each other but nothing else; objects are coerced via valueOf or toString.
=== does no coercion. If types differ, the result is false.
0 == "0"; // true — string coerced to number
0 == false; // true — boolean coerced to number
null == undefined; // true — special case
"1" == [1]; // true — array coerced
NaN == NaN; // false — NaN equals nothing, including itself
0 === "0"; // false — different types
null === undefined; // false
NaN === NaN; // false (same exception)
The rule: always use === unless you specifically need coercion. The few legitimate uses of ==: checking value == null to handle both null and undefined in one check.
Pointer: Topic 2.7, 2.8.
Q: Explain destructuring with renaming, defaults, and nested patterns.
Answer. Destructuring extracts values from objects or arrays into variables.
const user = { name: "Olena", age: 35, address: { city: "Redmond" } };
// Basic
const { name, age } = user;
// Rename
const { name: userName } = user;
// Default
const { name, role = "member" } = user;
// Nested
const { address: { city } } = user;
// Combined
const {
name: userName = "Anonymous",
address: { city: userCity = "Unknown" } = {}
} = user;
Same patterns work for arrays:
const [first, second, ...rest] = [1, 2, 3, 4, 5];
const [, , third] = [1, 2, 3]; // skip with commas
Destructuring in function signatures makes the expected shape visible:
function describeUser({ name, age = 0, role = "member" }) {
return `${name}, ${age}, ${role}`;
}
In our capstone, the createCache({ maxSize = 50, ttl = 60_000 } = {}) function uses every form — destructure, default per field, default the whole argument to {}. This pattern produces self-documenting function signatures.
Pointer: Topic 10.1.
Q: What does the ... operator do?
Answer. Two opposite things, depending on context.
Spread (in array/object literals or function calls) — expands an iterable into individual elements:
const combined = [...arr1, ...arr2]; // concatenate
const updated = { ...obj, key: "new" }; // immutable update
Math.max(...numbers); // pass array as args
Rest (in function parameters or destructuring) — collects multiple elements into one:
function sum(...nums) { return nums.reduce((t, n) => t + n, 0); }
const [first, ...rest] = [1, 2, 3, 4]; // first=1, rest=[2,3,4]
const { name, ...rest } = user; // name + everything else
The same syntax, opposite roles. Spread fans out. Rest collects.
In our capstone, both forms appear constantly: { ...this.#state, ...partial } for immutable state updates (spread), function debounce(fn, delay) { return function(...args) {...} } for the variadic wrapper (rest).
Pointer: Topic 10.2.
Q: Map vs Object — when to use each?
Answer. Both are key-value stores, but with important differences.
Use Map when:
- You need keys that aren't strings or symbols
- You add/remove keys frequently
- You need to know the size easily
- Iteration order matters (Map preserves strict insertion order)
Use Object when:
- Keys are always known string identifiers
- You need the object to be JSON-serializable
- You want the conciseness of object literal syntax
- You're representing a record/struct rather than a collection
Practical differences:
Objecthas a prototype chain (potential for inherited key collisions)Map.sizeis a direct property; counting Object keys requiresObject.keys(obj).length- Map's iteration order is guaranteed; Object's iteration has subtle rules around integer-like keys
- Map iteration via
for (const [k,v] of map)is more direct thanfor (const [k,v] of Object.entries(obj))
In our capstone, the cache uses Map because we exploit insertion-order behavior for LRU eviction and we need to add/remove keys frequently. The Store uses an Object for state because it's a fixed record shape that's easy to spread.
Pointer: Topic 10.4.
Cluster 6: Modules and the Ecosystem
Q: What's the difference between require and import?
Answer. require is CommonJS (Node's original module system). import is ES Modules (the language standard).
Differences:
- Synchronous vs static:
requireis a runtime function call;importis a static declaration parsed before code runs - Bindings vs snapshots: ES Module imports are live bindings into the source module's variables; CommonJS exports are snapshots taken at the moment of
require - Dynamic vs analyzable:
requirecan take a runtime-computed path;importrequires a static string literal (useawait import()for dynamic) - Tree-shaking: ES Modules' static structure enables dead-code elimination; CommonJS is harder to analyze
In Node, .mjs files are ES Modules, .cjs files are CommonJS, .js files depend on the "type" field in package.json.
Modern code defaults to ES Modules. Our capstone's package.json has "type": "module" for this reason.
Pointer: Topics 11.2, 11.5, 11.7.
Q: Default exports vs named exports — which is better?
Answer. Modern style guides prefer named exports for refactorability.
Default exports:
- At most one per module
- The importer chooses the name (can be anything)
- Easy to refactor incorrectly (different names across the codebase)
Named exports:
- Any number per module
- The importer must use the exact name (or explicitly rename)
- Enforced consistency across the codebase
- Better for static analysis and tools
Exception: React components are conventionally default exports — "one main thing per file" maps well to React's component-per-file pattern.
Our capstone uses named exports throughout. export function createCache, export class Store, etc. Searching for a function's usages is straightforward because the name is consistent everywhere.
Pointer: Topic 11.3.
Cluster 7: Browser, DOM, Performance
Interactive animation: The platform, closed: opening sentences for ESM vs CommonJS, event delegation, fetch on a 404, and memory leaks.
Q: Explain event delegation.
Answer. Event delegation is the pattern of attaching a single event listener to a parent element instead of attaching listeners to many children.
When an event fires on a child, it bubbles up through the DOM tree. The parent's listener catches it. event.target identifies which specific child was involved.
Benefits:
- Fewer listeners (one instead of N)
- Works with dynamically added children
- Easier cleanup (one listener to remove)
- Memory savings at scale
Trade-offs:
- Every event on the parent fires the handler — handler must check whether to act
stopPropagationfrom a child can interfere
In our capstone, setupFilters uses delegation for item clicks. One listener on the list container handles clicks on all items, current and future. event.target.closest(".item") finds the item that was clicked, even if the user clicked on a child of the item (like the <h3> inside it).
Pointer: Topic 12.4, 12.7.
Q: How does fetch differ from XMLHttpRequest?
Answer. fetch is the modern HTTP API. XMLHttpRequest is the legacy one.
Key differences:
fetchis Promise-based; XHR uses callback eventsfetchhas a cleaner API withHeaders,Request,Responseobjectsfetchdoes NOT throw on HTTP error status codes (you must checkresponse.ok)fetchdoesn't automatically send cookies cross-origin (usecredentials: "include")fetchsupports AbortController for cancellation; XHR has.abort()but less standardizedfetchdoesn't support upload progress events directly (XHR does)fetchis now standard in Node.js too (since ~2022)
In our capstone, fetchOnce checks response.ok and throws if false — without this check, an HTTP 500 would silently be processed as success because fetch doesn't throw on bad status codes. This is the single most common fetch bug.
Pointer: Topic 12.5.
Q: How would you find a memory leak in a Node.js or browser application?
Answer. The standard workflow:
- Identify the suspected leak scenario (an action that, when repeated, grows memory)
- Take a heap snapshot in DevTools' Memory panel (Node: use
--inspectand chrome://inspect) - Perform the suspect action several times
- Force GC if available, take another snapshot
- Compare snapshots — look for object types that grew significantly
- Drill into retained objects via the Retainers view to find the reference chain keeping them alive
Common leak patterns to check:
- Unbounded caches (no eviction)
- Forgotten event listeners (
addEventListenerwithoutremoveEventListener) - Detached DOM nodes (JS references to elements removed from the DOM)
- Long-lived closures holding large data
- Uncleared timers (
setIntervalwithoutclearInterval) - Global variable accumulation
In our capstone, we avoid leaks by: capping the cache (maxSize), cancelling abandoned fetches (AbortController), and using event delegation (one listener instead of many).
Pointer: Topic 13.5, 13.7.
Q: What are hidden classes in V8, and why do they matter for performance?
Answer. Hidden classes are V8's internal representation of object shapes. Objects with the same properties added in the same order share a hidden class. V8 uses this to optimize property access via inline caches.
const a = { x: 1, y: 2 }; // hidden class A
const b = { x: 3, y: 4 }; // same hidden class A
const c = { y: 5, x: 6 }; // different hidden class — different property order
For property access in hot code, the hidden class determines which inline cache is used. Stable hidden classes lead to fast monomorphic caches. Varied hidden classes lead to slow polymorphic or megamorphic caches.
The practical advice for hot paths:
- Initialize all properties in the constructor (no conditional property addition)
- Always set properties in the same order
- Don't use
deleteon properties - Avoid changing types of variables
For most application code, this doesn't matter — V8 handles cold code fine. For inner loops processing many objects, shape consistency can produce 10x performance differences.
In our capstone, the items returned from the API have consistent shape (always {id, name, price, rating}). This helps V8 optimize the render loop in renderList.
Pointer: Topic 13.3, 13.7.
How to Use This Bank
Three modes of use:
1. Final review before an interview. Scan the cluster(s) most relevant to the role. Read each question and try to answer out loud before reading the model answer.
2. Weekly practice. Pick 5 questions per session. Answer aloud, ideally recorded. Listen back — notice hesitation, vague phrasing, missed details.
3. Companion to mock interviews. When you stumble on a question in a mock, find the corresponding entry here and study the model answer.
The 80 questions in Topic 14.2 and the 30+ full answers in this Topic together cover essentially every JavaScript question you'll see in a senior or staff interview. They are the consolidated output of fifty-eight rejections across seven founders, applied to the question of "what do interviewers actually ask, and what's the senior answer?"
The course is done. Now go interview.
— Olena, Nikhil, Brandon, Emeka, and Arun, on behalf of the full AlgoInsight team