JS Machine Coding Patterns
The recurring "implement this from scratch" set — expect at least one of these in any JS machine-coding round.
Debounce vs. Throttle
Debounce: run only after the trigger has paused for delay ms — good for search-input, resize. Throttle: run at most once every limit ms — good for scroll, mousemove.
function debounce(func, delay) {
let timerId;
function debounced(...args) {
clearTimeout(timerId);
timerId = setTimeout(() => func.apply(this, args), delay);
}
debounced.cancel = () => clearTimeout(timerId);
debounced.flush = () => { /* invoke immediately if pending — track args/this to support this */ };
return debounced;
}
function throttle(func, limit) {
let throttled = false;
return function(...args) {
if (throttled) return;
func.apply(this, args);
throttled = true;
setTimeout(() => { throttled = false; }, limit);
};
}
One-liner to remember: debounce is "run after the pause"; throttle is "run at the start, then wait."
Currying
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) return func.apply(this, args);
return (...more) => curried.apply(this, args.concat(more));
};
}
// Infinite currying (sum that's also usable as a plain number via valueOf)
function sum(val) {
let total = val;
function inner(next) { total += next; return inner; }
inner.valueOf = () => total;
return inner;
}
sum(1)(2)(3) == 6; // true — valueOf is invoked implicitly in a numeric comparison context
func.length (the declared arity of the original function) is what tells the generic curry implementation how many arguments to wait for before actually invoking — this is the key insight that makes a generic curry function possible, rather than one hardcoded to a specific argument count.
Promise Polyfills
function promiseAll(iterable) {
return new Promise((resolve, reject) => {
const result = new Array(iterable.length);
let completed = 0;
if (iterable.length === 0) return resolve([]);
iterable.forEach((item, i) => {
Promise.resolve(item).then(value => {
result[i] = value;
if (++completed === iterable.length) resolve(result);
}).catch(reject);
});
});
}
function promiseAllSettled(iterable) {
return new Promise(resolve => {
const result = new Array(iterable.length);
let completed = 0;
if (iterable.length === 0) return resolve([]);
iterable.forEach((item, i) => {
Promise.resolve(item)
.then(value => { result[i] = { status: "fulfilled", value }; })
.catch(reason => { result[i] = { status: "rejected", reason }; })
.finally(() => { if (++completed === iterable.length) resolve(result); });
});
});
}
function promiseRace(iterable) {
return new Promise((resolve, reject) => {
iterable.forEach(item => Promise.resolve(item).then(resolve, reject));
});
}
Note promiseAll's .catch(reject) propagates the first rejection immediately (fail-fast), while allSettled's .finally always increments completed regardless of fulfillment/rejection — that structural difference (reject-and-stop vs. record-and-continue) is the entire semantic distinction between the two combinators, implemented in a few lines.
Array Method Polyfills
Array.prototype.myMap = function(callback, thisArg) {
const result = new Array(this.length);
for (let i = 0; i < this.length; i++) {
if (Object.hasOwn(this, i)) result[i] = callback.call(thisArg, this[i], i, this);
}
return result;
};
Array.prototype.myReduce = function(callback, initialVal) {
let result = initialVal;
let i = 0;
if (result === undefined) {
if (this.length === 0) throw new TypeError("Reduce of empty array with no initial value");
result = this[0]; i = 1;
}
for (; i < this.length; i++) result = callback(result, this[i], i, this);
return result;
};
Object.hasOwn(this, i) (checking for array "holes," see [[JS Core Language & Data Types]]) is what makes these polyfills correctly skip sparse-array gaps the same way the native methods do, rather than treating a hole as undefined.
Deep Clone
// Basic — JSON-serializable values only
function deepClone(value) {
if (typeof value !== "object" || value === null) return value;
if (Array.isArray(value)) return value.map(deepClone);
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, deepClone(v)]));
}
// Handles cycles + Date/RegExp/Map/Set — the real interview follow-up
function deepCloneWithCache(value, cache = new WeakMap()) {
if (typeof value !== "object" || value === null) return value;
if (cache.has(value)) return cache.get(value); // cycle detected — return the already-cloned ref
if (value instanceof Date) return new Date(value);
if (value instanceof RegExp) return new RegExp(value);
const cloned = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
cache.set(value, cloned); // register BEFORE recursing — this is what breaks infinite cycles
for (const key of Reflect.ownKeys(value)) {
cloned[key] = deepCloneWithCache(value[key], cache);
}
return cloned;
}
Why the cache must be set before recursing into children, not after: if object A references itself (directly or via B), the recursive call into A's own children needs to find A's clone already registered in the cache — registering it after fully cloning would mean the recursive call never terminates, since it'd try to clone A again from scratch every time it's encountered.
Custom getElementsByClassName (DOM Traversal)
function getElementsByClassName(element, classNames) {
const elements = [];
const classes = new Set(classNames.trim().split(/\s+/));
function helper(el) {
if ([...classes].every(c => el.classList.contains(c))) elements.push(el);
for (const child of el.children) helper(child);
}
for (const child of element.children) helper(child);
return elements;
}
A plain recursive tree walk — the same divide-and-conquer shape as most custom DOM-traversal machine-coding questions (getElementsByTagName, getElementsByStyle follow an identical structure, just swapping the match condition).
Interview Drills
- How does a generic
curry(func)implementation know how many arguments to collect before invoking the original function? —func.lengthgives the declared number of parameters of the original function — the curried wrapper compares the accumulated argument count against this to decide whether to invoke now or return another partially-applied function waiting for more arguments. - In a cycle-safe
deepClone, why must the new clone be registered in the cache before recursing into its children? — If object A (directly or transitively) references itself, the recursive call encountering A again needs to find its already-in-progress clone in the cache and return that reference immediately — registering only after the clone fully completes would mean a truly cyclic structure never terminates, since each encounter would restart cloning from scratch. - Why does
Promise.all's polyfill callrejecton the first rejection whileallSettled's polyfill never rejects at all? — This is the entire semantic difference between the two combinators, not an implementation detail —allis fail-fast (any single rejection aborts the whole combined promise), whileallSettledguarantees it always resolves once every input has settled, successful or not, which is exactly why its per-item handler always increments the completed counter in a.finally()rather than propagating a rejection.
Cross-links
[[JS Core Language & Data Types]] · [[JS Closures & Async]] · [[OOD Design Patterns]]