Junior JavaScript interviews ask "what does
vardo." Senior interviews ask why all your click handlers log the same number, why aPromise.thenruns before asetTimeout(0), and why your single-page app's memory climbs until the tab crashes after an hour.
JavaScript looks forgiving and turns out to be full of sharp edges — closures, this, the event loop, and the ways a long-lived SPA leaks memory — exactly where senior work lives. Seniority is knowing what the engine actually does, not just what the syntax says.
This is 30 questions across scope & closures, this & prototypes, async & the event loop, and functional patterns, memory & modern JS. Each has a short answer, a deep dive with code, and the senior signal the interviewer is listening for. Examples are grounded in Mattrx — a real long-lived React SPA where memory, the event loop, and bundle size are daily concerns.
Block A — scope, closures & types
1. var vs let vs const — what's actually different?
var is function-scoped and hoisted (initialized undefined); let/const are block-scoped and live in the temporal dead zone until their declaration (accessing early throws). const binding can't be reassigned (the value can still mutate). Default to const, use let when you must reassign, never var.
2. What is a closure, and why is it useful?
A function that remembers the variables of the scope it was created in, even after that scope returned — a live reference, not a copy. It's how JS does private state, factories, memoization, and every hook/callback that captures a value.
function makeCounter() {
let count = 0; // captured, kept alive by the closures below
return { inc: () => ++count, get: () => count };
}
const c = makeCounter();
c.inc(); c.inc(); c.get(); // 2 — count survives after makeCounter() returned
3. Why do all my loop handlers log the same number?
With var, the loop variable is one function-scoped binding shared by every closure — by the time the handlers run, the loop has finished and they all read the final value. let creates a fresh binding per iteration.
// BUG: var is function-scoped -> every handler closes over the SAME i.
for (var i = 0; i < buttons.length; i++)
buttons[i].onclick = () => console.log(i); // always logs buttons.length
// FIX: let is block-scoped -> a new binding each iteration.
for (let i = 0; i < buttons.length; i++)
buttons[i].onclick = () => console.log(i); // logs 0, 1, 2, ...
4. How does hoisting work?
Declarations are processed before code runs. function declarations hoist fully (callable before their line). var hoists the binding but not the value (reads undefined early). let/const hoist into the TDZ. It's scope setup, not "code moves up."
5. == vs === — and when is == ever okay?
=== compares value and type, no coercion. == coerces first, with surprising rules. Default to ===. The one common == idiom: x == null to catch both null and undefined.
6. Primitives vs reference types — how are they passed?
Primitives are copied by value. Objects/arrays/functions are held by reference — passing one copies the reference, so mutations inside are visible outside. JS is always pass-by-value; the value of an object is its reference.
7. null vs undefined vs NaN?
undefined = exists but no value (uninitialized, missing arg). null = an intentional "no value" you assigned. NaN = result of an invalid numeric op, and NaN !== NaN (use Number.isNaN).
8. How do you reliably check a value's type?
typeof for primitives (but typeof null === 'object' is a historical bug), Array.isArray() for arrays, instanceof for class instances (fails across realms), and Object.prototype.toString.call(x) as the catch-all.
Block B — this, prototypes & objects
9. How is this determined?
By the call site, not where the function is defined. Four rules by priority: new, explicit call/apply/bind, implicit (obj.method()), and default (undefined in strict mode). Arrow functions ignore all of this and inherit this lexically.
const dashboard = { tenant: 'acme', load() { return this.tenant; } };
dashboard.load(); // 'acme' (implicit: this = dashboard)
const fn = dashboard.load;
fn(); // undefined — this lost (bare call = default binding)
10. Arrow functions vs regular functions?
Arrows have no own this (lexical), no arguments, can't be newed, no prototype. Use arrows for callbacks where you want the surrounding this; regular functions for methods and constructors.
11. What do call, apply, and bind do?
All set this explicitly. call(thisArg, a, b) invokes now with args listed; apply(thisArg, [a, b]) invokes now with args as an array; bind returns a new function with this (and leading args) permanently fixed — it doesn't call.
12. Explain prototypal inheritance and the prototype chain.
Every object has a hidden link ([[Prototype]]) to another object. Property lookup walks this chain until found or it hits null. Methods live on the prototype, shared across instances.
instance --[[Prototype]]--> Campaign.prototype --> Object.prototype --> null
13. Is class real inheritance or just sugar?
Syntactic sugar over prototypes. class gives cleaner syntax for constructors, methods (on the prototype), extends, and super — but it's still prototype links underneath. It adds a few real things: methods are non-enumerable, and the body runs in strict mode.
14. Shallow vs deep copy — how do you copy an object?
Spread ({...obj}) and Object.assign are shallow — nested objects are still shared. For a deep copy use structuredClone(obj) (handles cycles, Dates, Maps) — not JSON.parse(JSON.stringify(...)), which drops functions, undefined, and Dates.
15. What does Object.freeze do (and its limits)?
Object.freeze(obj) makes own top-level properties non-writable/non-configurable — shallow immutability. Nested objects stay mutable (deep-freeze recursively). It's runtime enforcement; TS readonly is compile-time only.
16. What are getters/setters and property descriptors?
get/set let a property run a function on read/write while looking like a field. Object.defineProperty controls a property's descriptor — writable, enumerable, configurable, or accessor functions.
Block C — async & the event loop
17. Explain the event loop.
JS runs on one thread. Sync code runs on the call stack. When the stack empties, the event loop drains the entire microtask queue (Promises), then runs one macrotask (setTimeout/I/O/UI), then drains microtasks again, and repeats.
run sync (call stack) -> drain ALL microtasks -> run ONE macrotask -> drain ALL microtasks -> ...
18. What prints first: setTimeout(0) or Promise.then?
Promise.then. A resolved promise's callback is a microtask; setTimeout(0) is a macrotask. All microtasks run before the next macrotask.
console.log('A');
setTimeout(() => console.log('B'), 0); // macrotask
Promise.resolve().then(() => console.log('C')); // microtask
console.log('D');
// Output: A D C B
19. Promises — states and error propagation?
A promise is pending, then settles once to fulfilled or rejected (immutable after). A rejection skips .then handlers and jumps to the next .catch. Returning a promise from .then flattens. .finally runs either way.
20. What does async/await desugar to, and how do you handle errors?
async functions return a promise; await pauses until a promise settles, then resumes. Errors are handled with try/catch (a rejected awaited promise throws). An unawaited async call is a floating promise.
async function loadKpis(tenantId) {
try {
const res = await fetch('/api/kpis?t=' + tenantId);
if (!res.ok) throw new Error(res.status);
return await res.json();
} catch (e) {
logger.error('kpi load failed', e);
return null; // graceful fallback
}
}
21. Promise.all vs allSettled vs race vs any?
all — resolves when all succeed, rejects on first failure (fail-fast). allSettled — waits for all, never rejects, returns each status. race — settles as the first settles (success or failure). any — resolves on first success, rejects only if all fail.
22. What are the most common async mistakes?
await in a loop when calls are independent (serial instead of parallel), forgetting to await/return (floating), unhandled rejections, mixing callbacks with promises.
// SLOW: N serial round trips.
for (const id of campaignIds) results.push(await fetchStats(id));
// FAST: fire all, await together.
const results = await Promise.all(campaignIds.map(id => fetchStats(id)));
23. How do you convert a callback API to a promise?
Wrap it in new Promise, calling resolve/reject inside the callback (or util.promisify in Node).
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
await wait(300);
24. Implement debounce vs throttle — and when do you use each?
Debounce delays running until activity stops for N ms (search box). Throttle runs at most once per N ms during continuous activity (scroll/resize).
function debounce(fn, wait) {
let t;
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
}
const onSearch = debounce(q => fetchResults(q), 300);
Block D — functional patterns, memory & modern JS
25. map / filter / reduce — and immutability in practice?
map transforms each element, filter selects a subset, reduce folds a list into a single value. They return new arrays (immutable-friendly) — exactly what React state needs.
const byCampaign = events.reduce((acc, e) => {
(acc[e.campaignId] ??= []).push(e);
return acc;
}, {});
26. What are currying and function composition?
Currying turns f(a, b, c) into f(a)(b)(c) — fixing args one at a time. Composition chains small functions so one's output feeds the next (compose(f, g)(x) = f(g(x))).
27. What causes memory leaks in a browser SPA, and how do you prevent them?
Four causes: event listeners never removed, timers/intervals never cleared, closures that pin large objects, and detached DOM nodes still referenced. Clean up in your component's unmount/effect teardown.
useEffect(() => {
const onResize = () => layout(data);
window.addEventListener('resize', onResize);
const timer = setInterval(poll, 5000);
return () => { // WITHOUT this cleanup, every mount leaks
window.removeEventListener('resize', onResize);
clearInterval(timer);
};
}, [data]);
28. ESM vs CommonJS, and why does it matter for bundle size?
ESM (import/export) is static — the bundler can tree-shake unused code. CommonJS (require) is dynamic and resists tree-shaking. ESM also enables dynamic import() for code-splitting.
const Chart = React.lazy(() => import('./HeavyChart'));
29. What are generators and iterators?
An iterator is any object with a next() returning {value, done}; the iterable protocol (Symbol.iterator) is what for...of and spread consume. A generator (function* + yield) pauses and resumes, producing values lazily. async function* + for await...of streams async data.
30. What is event delegation and why use it?
Attach one listener to a common ancestor and use event.target to find which child fired — leveraging bubbling. Fewer listeners (less memory), and it handles elements added later automatically.
The through-line: JavaScript's ergonomics hide real machinery. Predict output from the event loop, reason about this from the call site, prevent leaks by cleaning up listeners/timers/references, and keep bundles lean with ESM + dynamic import. Answer each question with the engine behavior and the failure it prevents.
This post was originally published on PrepStack. The full original has all the code and senior-signal notes.
Top comments (1)
A closure is not a function, and ALL functions retain access to the variables in their lexical scope. If your definition were correct, there would be no point having separate words for closure and function.
dev.to/jonrandy/misconceptions-abo...