JavaScript Utility Functions: The Interview Set
A small set of utilities appears in frontend interviews again and again: debounce, throttle, memoise, curry, deep clone, flatten, event emitter, bind, and a handful of array helpers. Each one tests a specific idea (closures, this, recursion, edge cases). This chapter gives a clean, tested implementation of each, the reasoning, and the follow-up questions you should expect.
1. Debounce
Purpose: run a function only after calls have stopped for ms. Use it for search-as-you-type, resize handlers and autosave.
Idea: every call resets a timer; only the last timer survives. The timer ID lives in a closure.
function debounce(fn, wait) {
let timer;
function debounced(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait);
}
debounced.cancel = () => clearTimeout(timer);
return debounced;
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
const seen = [];
const d = debounce(v => seen.push(v), 20);
d(1); d(2); d(3);
await sleep(50);
assert.deepEqual(seen, [3]); // only the last call ran
d(4); d.cancel();
await sleep(40);
assert.deepEqual(seen, [3]); // cancelled before firing
Follow-ups: a leading-edge option (fire immediately on the first call, then ignore until quiet), a flush method, and preserving this and arguments (done above with apply and ...args).
2. Throttle
Purpose: run a function at most once every ms, however often it is called. Use it for scroll, mousemove and drag handlers.
function throttle(fn, wait) {
let last = -Infinity, timer, lastArgs, lastThis;
return function throttled(...args) {
const now = Date.now();
lastArgs = args; lastThis = this;
const remaining = wait - (now - last);
if (remaining <= 0) { // leading call
clearTimeout(timer); timer = undefined;
last = now;
fn.apply(lastThis, lastArgs);
} else if (!timer) { // schedule a trailing call with the latest arguments
timer = setTimeout(() => {
last = Date.now(); timer = undefined;
fn.apply(lastThis, lastArgs);
}, remaining);
}
};
}
const hits = [];
const t = throttle(v => hits.push(v), 30);
t("a"); t("b"); t("c"); // the first runs immediately; the latest runs at the end of the window
await sleep(60);
assert.deepEqual(hits, ["a", "c"]);
Debounce versus throttle: debounce waits for silence (one call after a burst); throttle guarantees a steady rate during a burst. Use requestAnimationFrame as a throttle for visual updates.
3. Memoise
Purpose: cache results of a pure function by its arguments.
function memoize(fn, keyFn = (...args) => JSON.stringify(args)) {
const cache = new Map();
return function (...args) {
const key = keyFn(...args);
if (cache.has(key)) return cache.get(key);
const value = fn.apply(this, args);
cache.set(key, value);
return value;
};
}
let computed = 0;
const slowSquare = memoize(n => { computed++; return n * n; });
assert.equal(slowSquare(9), 81);
assert.equal(slowSquare(9), 81);
assert.equal(computed, 1);
const fib = memoize(n => (n < 2 ? n : fib(n - 1) + fib(n - 2)));
assert.equal(fib(50), 12586269025); // linear time instead of exponential
Pitfalls: JSON.stringify keys ignore functions and order of keys and treat undefined oddly; memoising impure functions returns stale results; an unbounded cache leaks memory (use an LRU or WeakMap for object keys).
4. Curry and partial application
Curry turns f(a, b, c) into f(a)(b)(c), accepting arguments in groups until enough are collected (fn.length is the declared arity).
function curry(fn) {
return function curried(...args) {
return args.length >= fn.length
? fn.apply(this, args)
: (...more) => curried.apply(this, [...args, ...more]);
};
}
const add3 = curry((a, b, c) => a + b + c);
assert.equal(add3(1)(2)(3), 6);
assert.equal(add3(1, 2)(3), 6);
assert.equal(add3(1)(2, 3), 6);
const partial = (fn, ...preset) => (...rest) => fn(...preset, ...rest);
assert.equal(partial(Math.max, 10)(3, 7), 10);
5. Deep clone and deep equal
function deepClone(value, seen = new WeakMap()) {
if (value === null || typeof value !== "object") return value;
if (seen.has(value)) return seen.get(value); // handles circular references
if (value instanceof Date) return new Date(value);
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
if (value instanceof Map) {
const m = new Map(); seen.set(value, m);
value.forEach((v, k) => m.set(deepClone(k, seen), deepClone(v, seen)));
return m;
}
if (value instanceof Set) {
const s = new Set(); seen.set(value, s);
value.forEach(v => s.add(deepClone(v, seen)));
return s;
}
const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
seen.set(value, copy);
for (const key of Reflect.ownKeys(value)) copy[key] = deepClone(value[key], seen);
return copy;
}
const src = { n: 1, list: [1, { deep: true }], when: new Date(0), tags: new Set([1]) };
src.self = src; // circular
const copy = deepClone(src);
assert.notEqual(copy, src);
assert.notEqual(copy.list[1], src.list[1]);
assert.equal(copy.self, copy); // the cycle is preserved in the copy
assert.ok(copy.when instanceof Date && copy.when !== src.when);
function deepEqual(a, b) {
if (Object.is(a, b)) return true;
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
const ka = Object.keys(a), kb = Object.keys(b);
if (ka.length !== kb.length || Array.isArray(a) !== Array.isArray(b)) return false;
return ka.every(k => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a[k], b[k]));
}
assert.ok(deepEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }));
assert.ok(!deepEqual({ a: 1 }, { a: 1, b: undefined }));
In practice use structuredClone. JSON.parse(JSON.stringify(x)) drops functions, undefined, and Date becomes a string, and it throws on cycles.
6. Flatten
function flatten(arr, depth = Infinity) {
return arr.reduce(
(acc, item) => (Array.isArray(item) && depth > 0 ? acc.concat(flatten(item, depth - 1)) : acc.concat([item])),
[]
);
}
assert.deepEqual(flatten([1, [2, [3, [4]]]]), [1, 2, 3, 4]);
assert.deepEqual(flatten([1, [2, [3, [4]]]], 1), [1, 2, [3, [4]]]);
assert.deepEqual([1, [2, [3]]].flat(Infinity), [1, 2, 3]); // the built-in equivalent
// an iterative version avoids deep recursion
function flattenIter(arr) {
const stack = [...arr], out = [];
while (stack.length) {
const x = stack.pop();
Array.isArray(x) ? stack.push(...x) : out.push(x);
}
return out.reverse();
}
assert.deepEqual(flattenIter([1, [2, [3, [4]]], 5]), [1, 2, 3, 4, 5]);
7. Event emitter
The publish-subscribe pattern behind DOM events, Node's EventEmitter and many state libraries.
class Emitter {
#handlers = new Map();
on(event, fn) {
if (!this.#handlers.has(event)) this.#handlers.set(event, new Set());
this.#handlers.get(event).add(fn);
return () => this.off(event, fn); // return an unsubscribe function
}
off(event, fn) { this.#handlers.get(event)?.delete(fn); }
once(event, fn) {
const wrapper = (...args) => { this.off(event, wrapper); fn(...args); };
return this.on(event, wrapper);
}
emit(event, ...args) {
[...(this.#handlers.get(event) ?? [])].forEach(fn => fn(...args)); // copy so handlers can unsubscribe safely
}
}
const em = new Emitter(), got = [];
const off = em.on("x", v => got.push("a" + v));
em.once("x", v => got.push("once" + v));
em.emit("x", 1); em.emit("x", 2);
off(); em.emit("x", 3);
assert.deepEqual(got, ["a1", "once1", "a2"]);
8. bind, call and apply
Function.prototype.myBind = function (context, ...preset) {
const fn = this;
return function bound(...args) {
return fn.apply(this instanceof bound ? this : context, [...preset, ...args]); // works with new
};
};
function intro(greeting, punct) { return `${greeting}, ${this.name}${punct}`; }
assert.equal(intro.myBind({ name: "Asha" }, "Hi")("!"), "Hi, Asha!");
9. Array helpers
Array.prototype.myMap = function (cb, thisArg) {
const out = new Array(this.length);
for (let i = 0; i < this.length; i++) if (i in this) out[i] = cb.call(thisArg, this[i], i, this);
return out;
};
Array.prototype.myReduce = function (cb, init) {
let i = 0, acc = init;
if (arguments.length < 2) {
if (this.length === 0) throw new TypeError("Reduce of empty array with no initial value");
acc = this[i++];
}
for (; i < this.length; i++) acc = cb(acc, this[i], i, this);
return acc;
};
assert.deepEqual([1, 2, 3].myMap(x => x * 2), [2, 4, 6]);
assert.equal([1, 2, 3, 4].myReduce((s, x) => s + x), 10);
assert.throws(() => [].myReduce((s, x) => s + x), TypeError);
const groupBy = (arr, keyFn) => arr.reduce((acc, x) => { (acc[keyFn(x)] ??= []).push(x); return acc; }, {});
assert.deepEqual(groupBy([1, 2, 3, 4, 5], n => (n % 2 ? "odd" : "even")), { odd: [1, 3, 5], even: [2, 4] });
const unique = arr => [...new Set(arr)];
const chunk = (arr, n) => Array.from({ length: Math.ceil(arr.length / n) }, (_, i) => arr.slice(i * n, i * n + n));
assert.deepEqual(unique([1, 1, 2, 3, 3]), [1, 2, 3]);
assert.deepEqual(chunk([1, 2, 3, 4, 5], 2), [[1, 2], [3, 4], [5]]);
10. Get and set by path
function get(obj, path, fallback) {
const keys = Array.isArray(path) ? path : path.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
let cur = obj;
for (const k of keys) { if (cur == null) return fallback; cur = cur[k]; }
return cur === undefined ? fallback : cur;
}
const data = { a: { b: [{ c: 5 }] } };
assert.equal(get(data, "a.b[0].c"), 5);
assert.equal(get(data, "a.x.y", "none"), "none");
11. Common mistakes
- Forgetting
thisand arguments in debounce and throttle. - Creating the debounced function inside a render so each render makes a new timer (in React, memoise it or use a ref).
- Memoising impure functions, or with a key that cannot distinguish arguments.
- Using JSON cloning for data that contains dates,
undefined, maps, functions or cycles. - Not copying the handler list in
emit, which breaks when a handler unsubscribes during emission. - Ignoring empty-array and sparse-array edge cases in
reduceandmap. - Recursion depth problems in flatten and clone for very deep structures.
12. Practice questions
- Implement debounce with leading and trailing options and a
cancelmethod. - Implement throttle. How does it differ from debounce, and when would you use each?
- Write
memoize. What are the risks of an unbounded cache? - Implement
currysof(1)(2)(3),f(1, 2)(3)andf(1)(2, 3)all work. - Write a deep clone that supports circular references.
- Implement an event emitter with
on,off,onceandemit. - Implement
Function.prototype.bind. - Implement
Promise.alland a concurrency-limitedmap.