Loading article...
Fetching the blog content...
Loading article...
Fetching the blog content...
Every implementation worth having as muscle memory — debounce, throttle, Promise combinators, Array polyfills, bind/call/apply, deep clone, curry, memoize, EventEmitter, retry with backoff, and capped concurrency. Plus the concepts behind output-prediction questions.
Expect a mix of output-prediction ("what does this log?") and implement-from-scratch. The implementations below are the ones worth having as muscle memory. Each starts whiteboard-simple, with the follow-up edge cases interviewers push on noted right after — because the real signal is whether you reach for those yourself.
Delays execution until the user stops triggering the event for delay ms. Resets the timer on every call. Use for search-as-you-type, resize, autosave.
JSfile.js1function debounce(fn, delay) { 2 let timer; 3 return function (...args) { 4 clearTimeout(timer); 5 timer = setTimeout(() => fn.apply(this, args), delay); 6 }; 7}
Follow-ups they push on:
apply(this, args) and not just fn(...args)? To preserve the this context and arguments of the original call. Using a regular function (not an arrow) for the returned function is what lets this bind correctly to the caller.leading/immediate option (fire on the first call, then debounce):JSfile.js1function debounce(fn, delay, immediate = false) { 2 let timer; 3 return function (...args) { 4 const callNow = immediate && !timer; 5 clearTimeout(timer); 6 timer = setTimeout(() => { 7 timer = null; 8 if (!immediate) fn.apply(this, args); 9 }, delay); 10 if (callNow) fn.apply(this, args); 11 }; 12}
Guarantees fn runs at most once per interval, no matter how often it's triggered. Use for scroll, mousemove, anything firing rapidly where you want a steady cadence.
JSfile.js1function throttle(fn, interval) { 2 let lastTime = 0; 3 return function (...args) { 4 const now = Date.now(); 5 if (now - lastTime >= interval) { 6 lastTime = now; 7 fn.apply(this, args); 8 } 9 }; 10}
The distinction to say out loud: debounce waits for quiet (fires once after activity stops); throttle enforces a rate (fires regularly during activity). A timer-based throttle variant guarantees a trailing call so the final event isn't dropped — mention it exists.
Promise.all — resolves with an array of all results once every promise resolves; rejects immediately if any rejects.
JSfile.js1function promiseAll(promises) { 2 return new Promise((resolve, reject) => { 3 const results = []; 4 let completed = 0; 5 if (promises.length === 0) return resolve(results); 6 7 promises.forEach((p, i) => { 8 Promise.resolve(p) // wrap non-promise values 9 .then((value) => { 10 results[i] = value; // index keeps order, not completion order 11 completed++; 12 if (completed === promises.length) resolve(results); 13 }) 14 .catch(reject); // first rejection wins 15 }); 16 }); 17}
Key points to verbalize: results are stored by index so order is preserved regardless of which resolves first; Promise.resolve(p) wraps raw values so [1, fetch(...)] works; the empty-array case resolves immediately.
Promise.allSettled — never rejects; waits for all and reports each outcome as {status, value} or {status, reason}.
JSfile.js1function promiseAllSettled(promises) { 2 return Promise.all( 3 promises.map((p) => 4 Promise.resolve(p) 5 .then((value) => ({ status: 'fulfilled', value })) 6 .catch((reason) => ({ status: 'rejected', reason })) 7 ) 8 ); 9}
Promise.race — settles (resolve or reject) as soon as the first promise settles.
JSfile.js1function promiseRace(promises) { 2 return new Promise((resolve, reject) => { 3 promises.forEach((p) => Promise.resolve(p).then(resolve, reject)); 4 }); 5}
(Promise.any is the sibling worth a one-liner: resolves on the first fulfillment, ignores rejections, and rejects only if all reject — with an AggregateError.)
These test whether you understand this, callback signatures, and the accumulator pattern. Attach to the prototype.
JSfile.js1// map — returns a new array 2Array.prototype.myMap = function (callback, thisArg) { 3 const result = []; 4 for (let i = 0; i < this.length; i++) { 5 if (i in this) result[i] = callback.call(thisArg, this[i], i, this); 6 } 7 return result; 8}; 9 10// filter — keeps elements where callback is truthy 11Array.prototype.myFilter = function (callback, thisArg) { 12 const result = []; 13 for (let i = 0; i < this.length; i++) { 14 if (i in this && callback.call(thisArg, this[i], i, this)) { 15 result.push(this[i]); 16 } 17 } 18 return result; 19}; 20 21// reduce — folds the array into a single value 22Array.prototype.myReduce = function (callback, initialValue) { 23 let acc = initialValue; 24 let startIndex = 0; 25 if (arguments.length < 2) { // no initial value provided 26 if (this.length === 0) throw new TypeError('Reduce of empty array with no initial value'); 27 acc = this[0]; 28 startIndex = 1; 29 } 30 for (let i = startIndex; i < this.length; i++) { 31 acc = callback(acc, this[i], i, this); 32 } 33 return acc; 34};
The detail interviewers want on reduce: handle the missing-initial-value case (use the first element as the seed and start from index 1, and throw on an empty array with no seed).
All three control this. call and apply invoke immediately; bind returns a new function.
JSfile.js1// call — pass args individually 2Function.prototype.myCall = function (context, ...args) { 3 context = context || globalThis; 4 const fnKey = Symbol('fn'); // Symbol avoids clobbering existing keys 5 context[fnKey] = this; // 'this' is the function being called 6 const result = context[fnKey](...args); 7 delete context[fnKey]; 8 return result; 9}; 10 11// apply — same, but args come as an array 12Function.prototype.myApply = function (context, args = []) { 13 context = context || globalThis; 14 const fnKey = Symbol('fn'); 15 context[fnKey] = this; 16 const result = context[fnKey](...args); 17 delete context[fnKey]; 18 return result; 19}; 20 21// bind — returns a new function with 'this' (and optional preset args) locked in 22Function.prototype.myBind = function (context, ...boundArgs) { 23 const fn = this; 24 return function (...callArgs) { 25 return fn.apply(context, [...boundArgs, ...callArgs]); 26 }; 27};
The mechanism to explain: you temporarily attach the function as a property of the context object, call it as a method (so this becomes the context), then clean up. Using a Symbol key avoids overwriting a real property. bind supports partial application — args passed at bind time prepend args passed at call time.
JSfile.js1function deepClone(value, seen = new WeakMap()) { 2 // primitives and functions: return as-is 3 if (value === null || typeof value !== 'object') return value; 4 5 // handle circular references 6 if (seen.has(value)) return seen.get(value); 7 8 if (value instanceof Date) return new Date(value); 9 if (value instanceof RegExp) return new RegExp(value.source, value.flags); 10 11 const clone = Array.isArray(value) ? [] : {}; 12 seen.set(value, clone); // register BEFORE recursing — breaks cycles 13 14 for (const key of Reflect.ownKeys(value)) { 15 clone[key] = deepClone(value[key], seen); 16 } 17 return clone; 18}
The two things that separate a strong answer: the WeakMap for circular references (register the clone before you recurse into children) and handling special objects like Date/RegExp. Mention structuredClone() exists natively now but doesn't clone functions and throws on them — good to know the tradeoff.
JSfile.js1function deepEqual(a, b) { 2 if (a === b) return true; // same reference or same primitive 3 if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) { 4 return false; 5 } 6 const keysA = Object.keys(a); 7 const keysB = Object.keys(b); 8 if (keysA.length !== keysB.length) return false; 9 10 return keysA.every((key) => 11 Object.prototype.hasOwnProperty.call(b, key) && deepEqual(a[key], b[key]) 12 ); 13}
Transforms f(a, b, c) into f(a)(b)(c) — collect args until you have enough, then invoke.
JSfile.js1function curry(fn) { 2 return function curried(...args) { 3 if (args.length >= fn.length) { // fn.length = number of declared params 4 return fn.apply(this, args); 5 } 6 return (...next) => curried.apply(this, [...args, ...next]); 7 }; 8} 9 10// usage 11const sum = (a, b, c) => a + b + c; 12const cSum = curry(sum); 13cSum(1)(2)(3); // 6 14cSum(1, 2)(3); // 6 15cSum(1)(2, 3); // 6
The trick is fn.length tells you the arity; keep accumulating args until you meet it.
Caches results keyed by arguments so repeated calls are instant.
JSfile.js1function memoize(fn) { 2 const cache = new Map(); 3 return function (...args) { 4 const key = JSON.stringify(args); // simple key; fine for primitive args 5 if (cache.has(key)) return cache.get(key); 6 const result = fn.apply(this, args); 7 cache.set(key, result); 8 return result; 9 }; 10}
Caveat to mention: JSON.stringify keys are fine for primitive arguments but break for functions/circular structures; for single-object args a WeakMap is better and lets entries be garbage-collected.
JSfile.js1function flatten(arr, depth = Infinity) { 2 return arr.reduce((acc, item) => { 3 if (Array.isArray(item) && depth > 0) { 4 acc.push(...flatten(item, depth - 1)); 5 } else { 6 acc.push(item); 7 } 8 return acc; 9 }, []); 10} 11// flatten([1, [2, [3, [4]]]]) -> [1, 2, 3, 4] 12// flatten([1, [2, [3]]], 1) -> [1, 2, [3]]
Bonus framing: a non-recursive version using a stack avoids deep-recursion stack overflow on huge inputs.
JSfile.js1function groupBy(arr, keyFn) { 2 return arr.reduce((groups, item) => { 3 const key = typeof keyFn === 'function' ? keyFn(item) : item[keyFn]; 4 (groups[key] ||= []).push(item); 5 return groups; 6 }, {}); 7} 8// groupBy([6.1, 4.2, 6.3], Math.floor) -> { 4: [4.2], 6: [6.1, 6.3] }
A core pattern. Register handlers by event name, emit to fire them all, support unsubscribe.
JSfile.js1class EventEmitter { 2 constructor() { 3 this.events = {}; // { eventName: [handlers] } 4 } 5 6 on(event, handler) { 7 (this.events[event] ||= []).push(handler); 8 return () => this.off(event, handler); // return an unsubscribe function 9 } 10 11 off(event, handler) { 12 if (!this.events[event]) return; 13 this.events[event] = this.events[event].filter((h) => h !== handler); 14 } 15 16 emit(event, ...args) { 17 (this.events[event] || []).forEach((h) => h(...args)); 18 } 19 20 once(event, handler) { 21 const wrapper = (...args) => { 22 handler(...args); 23 this.off(event, wrapper); 24 }; 25 this.on(event, wrapper); 26 } 27}
The nice touches that signal seniority: on returns an unsubscribe function, and once wraps the handler to auto-remove itself after firing.
Retry a failing async operation, waiting longer between each attempt (delay, delay*2, delay*4...).
JSfile.js1function retry(fn, retries = 3, delay = 500) { 2 return new Promise((resolve, reject) => { 3 function attempt(remaining, wait) { 4 fn() 5 .then(resolve) 6 .catch((err) => { 7 if (remaining === 0) return reject(err); 8 setTimeout(() => attempt(remaining - 1, wait * 2), wait); 9 }); 10 } 11 attempt(retries, delay); 12 }); 13} 14 15// usage 16retry(() => fetch('/api/flaky').then((r) => r.json()), 3, 500) 17 .then(console.log) 18 .catch((err) => console.error('all retries failed', err));
Why backoff: hammering a struggling server makes things worse; doubling the wait gives it room to recover. In production you'd add jitter (randomize the delay slightly) so many clients don't retry in sync.
These four are what output-prediction questions actually test. Be crisp.
A function "remembers" the scope it was created in, even after that outer scope has returned. The variables stay alive.
JSfile.js1function counter() { 2 let count = 0; 3 return () => ++count; // closes over `count` 4} 5const inc = counter(); 6inc(); // 1 7inc(); // 2 — same `count` persists
The classic trap (var in a loop):
JSfile.js1for (var i = 0; i < 3; i++) { 2 setTimeout(() => console.log(i), 0); // logs 3, 3, 3 3} 4// `var` is function-scoped — all callbacks share ONE `i`, which is 3 by the time they run. 5 6for (let i = 0; i < 3; i++) { 7 setTimeout(() => console.log(i), 0); // logs 0, 1, 2 8} 9// `let` is block-scoped — a fresh binding per iteration.
this binding rulesResolve this by how the function is called, in this priority:
new — this is the brand-new object.call/apply/bind set it directly.obj.method(), this is obj.this is undefined (strict mode) or the global object.Arrow functions ignore all of this — they have no own this; they capture it lexically from where they were defined. That's why arrows are great for callbacks (preserve this) and wrong for object methods that need their own this.
JSfile.js1const obj = { 2 name: 'Example', 3 regular() { return this.name; }, // 'Example' — implicit binding 4 arrow: () => this.name, // undefined — `this` is outer scope, not obj 5};
Objects link to other objects via [[Prototype]]. Property lookups walk the prototype chain until found or until null. Class syntax is sugar over this.
JSfile.js1function Animal(name) { this.name = name; } 2Animal.prototype.speak = function () { return `${this.name} makes a sound`; }; 3 4function Dog(name) { Animal.call(this, name); } // inherit instance props 5Dog.prototype = Object.create(Animal.prototype); // inherit methods 6Dog.prototype.constructor = Dog; 7 8new Dog('Rex').speak(); // "Rex makes a sound" — found up the chain
obj.hasOwnProperty(x) is true only for own props; the in operator also checks the chain.
var declarations are hoisted and initialized to undefined — you can reference one before its line without error (you just get undefined).let/const are hoisted too, but not initialized. The span from the top of the block to the declaration is the Temporal Dead Zone; touching the variable there throws ReferenceError.JSfile.js1console.log(a); // undefined (var hoisted, value not yet assigned) 2var a = 1; 3 4console.log(b); // ReferenceError — b is in the TDZ 5let b = 2;
JSfile.js1// callback hell 2getUser(id, (user) => { 3 getOrders(user, (orders) => { 4 getDetails(orders[0], (details) => console.log(details)); 5 }); 6}); 7 8// flattened with promises 9getUser(id) 10 .then(getOrders) 11 .then((orders) => getDetails(orders[0])) 12 .then(console.log) 13 .catch(handleError); 14 15// async/await — reads top to bottom 16async function load(id) { 17 try { 18 const user = await getUser(id); 19 const orders = await getOrders(user); 20 return await getDetails(orders[0]); 21 } catch (err) { 22 handleError(err); 23 } 24}
JSfile.js1// SEQUENTIAL — each await blocks the next. Total time = sum of all. Usually wrong here. 2async function slow(ids) { 3 const out = []; 4 for (const id of ids) out.push(await fetchItem(id)); // one at a time 5 return out; 6} 7 8// PARALLEL — fire all, then await together. Total time = the slowest one. 9async function fast(ids) { 10 return Promise.all(ids.map((id) => fetchItem(id))); // all at once 11}
The rule: await inside a loop = sequential. If the calls are independent, kick them all off and Promise.all the results. Use sequential only when each step depends on the previous one's output.
A very common question. You want parallelism, but capped (e.g. don't open 1000 connections at once). Maintain a pool of limit active workers pulling from a shared queue.
JSfile.js1async function asyncPool(limit, items, iteratorFn) { 2 const results = []; 3 const executing = new Set(); 4 5 for (const item of items) { 6 const p = Promise.resolve().then(() => iteratorFn(item)); 7 results.push(p); 8 executing.add(p); 9 // when this task finishes, remove it from the active set 10 p.finally(() => executing.delete(p)); 11 12 // if we're at capacity, wait for the fastest one to free a slot 13 if (executing.size >= limit) { 14 await Promise.race(executing); 15 } 16 } 17 return Promise.all(results); 18} 19 20// usage: fetch 100 URLs, max 5 in flight at any moment 21asyncPool(5, urls, (url) => fetch(url).then((r) => r.json()));
The mechanism to explain: each task adds itself to an executing set and removes itself when done. Once the set hits limit, Promise.race blocks the loop until any one finishes, freeing a slot for the next.
Implementations: debounce, throttle, Promise.all/allSettled/race, map/filter/reduce polyfills, bind/call/apply, deepClone, deepEqual, curry, memoize, flatten, groupBy, EventEmitter, retry with backoff, asyncPool (concurrency limit).
Concepts: closures (and the var-in-loop trap), this binding priority (new → explicit → implicit → default, arrows are lexical), prototypal inheritance and the chain, hoisting + the TDZ.
Async: sequence vs parallel (await in a loop is the trap), callback → promise → async/await, and capping concurrency.
Goal: Have every common JS coding interview implementation cold — from debounce and throttle through Promise combinators, polyfills, deep clone, curry, and async concurrency — plus the closures, this-binding, hoisting, and prototype concepts behind output-prediction questions.
Continue learning with these related challenges
The mental models and edge cases that separate someone who has used HTML, CSS, JS, forms, responsive design, and HTTP from someone who actually understands them. Part 1 of the Frontend Roadmap series.
Complete guide to Turborepo setup and migration. Learn intelligent caching, parallel task execution, and monorepo best practices to speed up your frontend builds dramatically.
Implement a retry function that handles transient failures by retrying async operations with optional delays.
The mental models and edge cases that separate someone who has used HTML, CSS, JS, forms, responsive design, and HTTP from someone who actually understands them. Part 1 of the Frontend Roadmap series.
Complete guide to Turborepo setup and migration. Learn intelligent caching, parallel task execution, and monorepo best practices to speed up your frontend builds dramatically.
Implement a retry function that handles transient failures by retrying async operations with optional delays.