Implement Debounce
Interview Question: "Implement a debounce function that delays invoking fn until after delay ms have elapsed since the last invocation."
The Core Implementation
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}Key points to articulate:
- Closure over
timeoutIdâ each debounced function has its own timer clearTimeoutbeforesetTimeoutâ resets the clock on each callfn.apply(this, args)â preservesthiscontext and arguments- Returns a new function (higher-order function)
Production Version (with Cancel + Flush + Leading)
function debounce(fn, delay, { leading = false } = {}) {
let timeoutId;
let lastArgs;
let lastThis;
function invoke() {
fn.apply(lastThis, lastArgs);
lastArgs = lastThis = undefined;
}
function debounced(...args) {
lastArgs = args;
lastThis = this;
if (leading && !timeoutId) {
invoke();
}
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
if (!leading) invoke();
timeoutId = undefined;
}, delay);
}
debounced.cancel = () => {
clearTimeout(timeoutId);
timeoutId = lastArgs = lastThis = undefined;
};
debounced.flush = () => {
if (timeoutId) {
clearTimeout(timeoutId);
invoke();
timeoutId = undefined;
}
};
return debounced;
}Follow-Up: "What's the difference between leading and trailing?"
- Trailing (default): Fires after silence. User stops typing â function runs.
- Leading: Fires immediately on first call, then ignores until delay passes. Button click â instant response, ignores rapid clicks.
Common Mistakes
- Forgetting
clearTimeoutâ timer stacks instead of resets - Losing
thiscontext â using arrow function for the outer return loses caller'sthis - Not handling
cancelâ real debounce functions need cleanup for unmounting