Creating "debounce()" Using a Closure

Creating "debounce()" Using a Closure

A close look at closures in JavaScript

·

6 min read

A debounce() function is one of those awesome tools that we reach for when we want to delay an action from taking place too rapidly.

A good use case might be trying to prevent too many API calls to your server when a user types into a search input box and an API call is fired for every key press. There may be some use cases where we want to immediately respond like this, but in many situations, we probably want to delay for a split second and check that the user hasn't in fact typed anything additional.

We can do this using a debounce() function. Each time we respond to an action, we wait for say 500ms and check that no more actions have been made before responding.

We'll first create a simple debounce() function to understand how it actually debounces an action. Then we'll look into closures and how they can help improve the implementation of debounce().

A Simple debounce() function

This simple implementation of the debounce() function uses three parameters.

  • callback() - the function that is called when the delay is over

  • currentId - which holds a reference to the current delayed action

  • delay - in miliseconds.

function debouce(callback, currentId, delay){
  window.clearTimeout(currentId);
  newId = window.setTimeout(function(){
      callback.apply();
  }, delay);
  return newId;
}

Each time the debounce() function is called, it will cancel any unresolved timeouts that have been passed in with the reference of currentId. It then creates a new timeout which is stored in the newId variable. The new timeout will then call the callback function when the delay is finished, providing it is not cancelled by calling debounce() again. We make the newId timeout reference available to the calling scope by returning the value of newId.

In the editor below, we can see that we have first defined a timeoutId. This is passed to debounce() function call along with the sayHello callback and a delay of 2000. We have then reassigned the value of timeoutId the value that we return from the debounce() function.

The console shows the following results:

debounced
debounced
debounced
hello

The word debounced is logged 3 times immediately and then 2 seconds later the word hello is logged.

Each time the debounce() function is called it cancels the previous timeout and sets up a new timeout to call the callback function after the delay.

This is a fairly simple solution, but it requires the timeoutId from any previous timeouts to be stored and passed to the debounce() function each time it is called.

We can develop a much cleaner and encapsulated approach using a closure.

A Close Look at Closures in JavaScript

Let's start with a definition of what closures are in JavaScript.

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time. MDN: Closures

Nothing like a concrete example to help us understand what that means. Look at the example in the editor below.

We define a function myFunc (the outer function) which performs three tasks:

  • logs out the word constructed

  • sets an initial value of count = 0

  • returns an anonymous function (the inner function)

We then call the myFunc() function and set its return value to the variable myFuncInstance

Looking at what is logged to the console, we can see that only the word constructed is logged to the console. The inner anonymous function is not called, it is only returned.

*Uncomment the next line so that we can see the value of myFuncInstance*

The value logged is:

function(){
    count++;
    console.log('called',count);
}

So the line const myFuncInstance = myFunc(); is in fact defining a new function expression.

const myFuncInstance = function(){
    count++;
    console.log('called', count); 
}

At this point, our new function expression has not been called, although we have defined it by calling myFunc().

At this stage, we have not done much more than return a function from a function. It does get interesting now as we look at the special behaviour of closures in JavaScript.

Uncomment the next three lines so that we are calling the myFuncInstance() three times.

Our console now logs:

constructed
function(){
    count++;
    console.log('called',count);
}
called 1
called 2
called 3

Each time we have called the myFuncInstance() function, we have been able to keep track of the value of count and correctly increment it.

This feels a bit strange for two reasons.

  • Why doesn't the count value get set back to 0 each time?

  • It seems that myFuncInstance() shouldn't have access to the value of count . Why don't we get undefined for the value of count as?

The reason for both of these behaviours is because of closures.

The myFunc() function is only actually called once and this is why the value is only set to 0 when we call const myFuncInstance = myFunc();

Again looking at our original myFunc() definition. We can see that in the function definition, the inner function does in fact have access to the value of count from the parent (outer) function When we call myFunc() and return the inner anonymous function, the inner function encloses the value of count and maintains its reference to this variable. Then every time we call myFuncInstance() this reference to the current value of count is correctly maintained.

function myFunc () {
  console.log('constructed');
  let count = 0;

  return function(){
    count++;
    console.log('called',count);
  };
}

This makes using closures a great way for calling functions and maintaining their state without needing to rely on any external variables.

debounce() with a closure

As we have just seen using a closure can help us to fully encapsulate the logic of a function without needing to refer to external variables..

function debounce(callback, wait){
  let timeoutId = null;
  return function(...args){
    window.clearTimeout(timeoutId);
    timeoutId = window.setTimeout(() => {
      callback(...args);
    }, wait);
  };
}

The first thing that is obvious is that we are not passing in the currentId of the current timeout. Instead, we create a closure whereby we define the timeoutId only once and then return a function that creates a closure around the timeoutId variable.

function sayHello(boy, girl){
   console.log(`Hello ${boy} & ${girl}`);
}

const debounced = debounce(sayHello, 5000);

debounced('Jack','Jill');
debounced('Sam', 'Maisy');
debounced('Peter', 'Jane');

Similar to the closures example that we looked at in the last section, we use a function expression to create our debounced() function.

const debounced = debounce(sayHello, 5000);

This time instead of calling the debounce() function multiple times, we instead call the newly created debounced() function.

This allows us to call the debounced() function without needing to keep track of the timeoutId, this has now been cleanly handled by the closure that we have created inside the debounce() function.

We have also added the ability to pass values to the debounced() function. When we return the anonymous function from the debounce() function definition, we have added ...args as a parameter to this function. This takes any parameters that are passed into the debounced() function debounced(A,B,C) and stores them in a new array.

We then have the option to use this array and either call this as the second parameter on the apply() method.

callback.apply(null, args);

Or alternatively, we can use the ...args with the spread operator and pass this directly on the callback() function call.

callback(...args);

Have a play around with the editor below