Debouncing controls how often a function can execute over time. When a JavaScript function is debounced with a wait time of wait milliseconds, it runs only after wait milliseconds have elapsed since the debounced function was last called.
You have probably encountered debouncing in daily life before, such as when entering an elevator. Only after some time passes without pressing the "Door open" button (the debounced function not being called) will the elevator door actually close (the callback function is executed).
Implement debounce(func, wait) so that func is called only after wait milliseconds have passed since the most recent call. The returned function should not invoke func immediately. When the delayed call finally runs, it should use the latest arguments and preserve the this value from the most recent call.
func (Function): The callback to debounce.wait (number): The number of milliseconds to wait after the latest call.(Function): Returns the debounced function.
let i = 0;function increment() {i++;}const debouncedIncrement = debounce(increment, 100);// t = 0: Call debouncedIncrement().debouncedIncrement(); // i = 0// t = 50: i is still 0 because 100ms have not passed.// t = 100: increment() was invoked and i is now 1.
debouncedIncrement() is called multiple times.
let i = 0;function increment() {i++;}const debouncedIncrement = debounce(increment, 100);// t = 0: Call debouncedIncrement().debouncedIncrement(); // i = 0// t = 50: i is still 0 because 100ms have not passed.// Call debouncedIncrement() again.debouncedIncrement(); // i = 0// t = 100: i is still 0 because it has only// been 50ms since the last debouncedIncrement() at t = 50.// t = 150: Because 100ms have passed since// the last debouncedIncrement() at t = 50,// increment was invoked and i is now 1.
cancel() method to cancel delayed invocations and a flush() method to immediately invoke them.Debounce is about keeping one pending trailing call. The property is that, at any moment, the closure stores either no timeout or exactly one timeout representing the latest invocation. The wrapper does not call func immediately. Instead, every invocation schedules func to run after wait milliseconds, and any newer invocation replaces the older schedule.
The closure only needs one durable piece of state: the current timeout ID. Each wrapper call also creates short-lived context and args values for that scheduled attempt. Since the previous timeout is cleared before a new one is scheduled, only the latest call's this value and arguments can reach func. Debounce is not "delay every call"; it is "replace the pending call until the input has been quiet for wait milliseconds."
this value and arguments.clearTimeout().wait milliseconds.func with Function.prototype.apply() so the delayed call keeps the original call shape.Calling func(...args) would forward the arguments but lose the dynamic this value. func.apply(context, args) forwards both.
If calls happen at t = 0 and t = 50 with wait = 100, the first timeout for t = 100 is canceled. The only callback that can run is the one scheduled by the latest call, at t = 150.
/*** @param {(...args: Array<unknown>) => unknown} func* @param {number} wait* @returns {(...args: Array<unknown>) => void}*/export default function debounce(func, wait = 0) {let timeoutID = null;return function (...args) {// Keep a reference to `this` so that// func.apply() can access it.const context = this;clearTimeout(timeoutID);timeoutID = setTimeout(function () {timeoutID = null; // Not strictly necessary but good to do this.func.apply(context, args);}, wait);};}
The default implementation stores this in a context variable before scheduling the timeout. Another valid option is to use an arrow function for the setTimeout callback, because arrow functions capture this lexically from the surrounding wrapper function.
With that approach, func.apply(this, args) still uses the this value from the wrapper call:
type AnyFunction = (this: any, ...args: any[]) => any;export default function debounce<T extends AnyFunction>(func: T,wait: number = 0,): (this: ThisParameterType<T>, ...args: Parameters<T>) => void {let timeoutID: ReturnType<typeof setTimeout> | null = null;return function (this: ThisParameterType<T>, ...args: Parameters<T>) {clearTimeout(timeoutID ?? undefined);timeoutID = setTimeout(() => {timeoutID = null; // Not strictly necessary but good to include.// Has the same `this` as the outer function's// as it's within an arrow function.func.apply(this, args);}, wait);};}
This is only safe for the timeout callback. The returned debounced function itself should not be an arrow function, because its this must be determined when callers invoke it.
Not canceling the previous timeout: Without clearTimeout(), every invocation eventually runs. Debounce needs the latest invocation to replace any earlier scheduled work.
Losing the latest arguments or receiver: The callback should run with the latest invocation's arguments and this value. Store them for the current scheduled attempt, and call the original function with apply() or call().
Returning an arrow function wrapper: The wrapper returned by debounce() should be a normal function. An arrow wrapper would not get a dynamic this, so calls like obj.debouncedMethod() would not preserve obj for the delayed callback.
setTimeoutthis worksclearTimeout() is forgiving: passing an invalid ID is a no-op. There is no need to guard against timeoutID being unset before clearing it.timeoutID to null after the callback runs is not strictly necessary, but it keeps the closure's state accurate after there is no pending timeout.wait value of 0 still schedules the callback through setTimeout(), so func is not called synchronously.cancel(), flush(), or leading-edge execution.console.log() statements will appear here.