Debouncing and closure

Implementing debounce with closures and function concepts

By Hank Kim

Debouncing and closure

Implementing debounce with closures and function concepts

Debounce in JavaScript

In this document, I want to explain what debounce is, why closure is necessary for it, and how concepts like this, arguments, and .apply are used in a typical debounce implementation.

1. What is Debounce?

Debounce ensures that when an event fires repeatedly in a short time, only the last invocation is executed after a delay.

Typical Use Cases

  • Search input autocomplete (wait until the user stops typing before calling the API)
  • Window resize event optimization

2. Why Closure is Required

A debounce function needs to remember the previous timer between calls, so it can cancel the old one and schedule a new one.

  • JavaScript local variables disappear after a function finishes.
  • To persist the timeout variable across multiple calls, it must live in the outer scope.
  • This is exactly what a closure provides: the returned function continues to reference variables defined in the parent scope.

3. Basic Implementation

Without Closure ❌

function badDebounce(fn, delay) {
  return function (...args) {
    let timeout = null; // re-created every call ❌
    clearTimeout(timeout); // always null
    timeout = setTimeout(() => fn(...args), delay);
  };
}
  • timeout is declared inside the returned function.
  • Every call creates a new timeout, so the previous one cannot be cleared.
  • Result: debounce doesn’t work.

With Closure ✅

function debounce(fn, delay) {
  let timeout; // captured by closure
  return function (...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn(...args), delay);
  };
}
  • timeout exists in the outer scope of the returned function.
  • Every call shares the same timeout variable.
  • The closure allows cancelling the previous timer and scheduling a new one.

4. Handling this, arguments, and .apply

A real-world debounce implementation must handle three details:

1. this (Execution Context)

In JavaScript, the value of this depends on how a function is called:

obj.method(); // this = obj
func(); // this = window (or undefined in strict mode)
button.onclick = fn; // this = button

If a method like obj.inc is wrapped by debounce, calling func() directly inside setTimeout would lose the original this.

Solution: store this in a variable and restore it when calling the function.


2. arguments

arguments is an array-like object holding all arguments passed to the function:

function test(a, b) {
  console.log(arguments[0]); // a
  console.log(arguments[1]); // b
}

In debounce, we capture arguments so the delayed function receives the same parameters as the original call.


3. .apply

apply allows calling a function with an explicit this and arguments array:

func.call(context, arg1, arg2);
func.apply(context, [arg1, arg2]);

Debounce uses func.apply(context, args) so that:

  • func is called with the original this context
  • args are passed exactly as received

5. Full Example

function debounce(func, delay) {
  let timeout;
  return function () {
    const context = this; // preserve this
    const args = arguments; // preserve arguments
    clearTimeout(timeout);
    timeout = setTimeout(
      () => func.apply(context, args), // restore this + arguments
      delay
    );
  };
}

6. Why This Matters

Example: Method Inside an Object

const obj = {
  value: 0,
  inc() {
    this.value++;
    console.log("value:", this.value);
  },
};

obj.debouncedInc = debounce(obj.inc, 500);
obj.debouncedInc();
  • When obj.debouncedInc() is called:

    • this = obj inside the wrapper
    • Stored as context = obj
    • After delay, func.apply(context, args) executes → this.value++ works correctly

If this is Not Preserved ❌

timeout = setTimeout(() => func(args), delay);
  • func() runs as a plain function
  • this becomes undefined (strict mode)
  • func code that uses this breaks

7. Role of Closure in Debounce

  • timeout is defined once in the outer scope of debounce
  • The returned function closes over timeout
  • This allows:

    • Canceling the old timer with clearTimeout(timeout)
    • Scheduling a new one with setTimeout
  • Without closure, debounce would fail to cancel prior timers