DEV Community

Cover image for How to use JavaScript closures with confidence
Irena Popova 👩🏻‍💻
Irena Popova 👩🏻‍💻

Posted on

How to use JavaScript closures with confidence

Understanding JavaScript Closures

Closure is all around you in JavaScript, you just have to recognize it.

Closures are a fundamental concept in JavaScript that allow functions to retain access to their lexical scope even when they are executed outside of that scope. This capability is essential for managing state, creating private variables, and implementing data encapsulation.

When you write in JavaScript, closures just happen, wether you are aware of it or not. Understanding closures to intentionally create them and leverage their power is a challenge that every JavaScript developer has to tackle; let’s give it a try.

Here’s a somewhat academic definition that will help you understand and spot a closure when you see one : closure is when a function is able to remember and access the variables of the outer (enclosing) function even when that function is executing outside its scope.

Closure is when a function is able to remember and access the variables of the outer (enclosing) function even when that function is executing outside its scope.

We will go back to the scope concept later. For the moment remember that, depending on the position of a variable in the code, some functions will have access to it and some will not.

Now about the variables; as you already may know, a variable has two components : a name and a value. The name “variable” makes it very clear : its value can vary from one moment to the next. But sometimes, we need to keep the value as it was at a certain point in time to use it later.

In JavaScript, this situation is frequently encountered when working with functions like setTimeout() and setInterval() for example.

So What is a Closure?

A closure is formed when a function is defined within another function, allowing the inner function to access variables from the outer function's scope. This means that the inner function "remembers" the environment in which it was created, even after the outer function has finished executing.

function outerFunction() {
    let outerVariable = 'I am from outer scope!';

    function innerFunction() {
        console.log(outerVariable); // Accessing outerVariable
    }

    return innerFunction;
}

const myClosure = outerFunction();
myClosure(); // Output: I am from outer scope!

Enter fullscreen mode Exit fullscreen mode

In this example, innerFunction is a closure that retains access to outerVariable even after outerFunction has executed.
Benefits of Using Closures
Data Privacy: Closures can be used to create private variables that cannot be accessed from outside the function. This is useful for encapsulating data and preventing unintended interference.

The for loop example

Let’s take the example of a for loop used to display a counter, and a setTimout() function used to delay the execution of a function.

Understanding the For Loop with setTimeout

When using a for loop in conjunction with setTimeout, many developers expect the loop to print numbers sequentially with a delay. However, due to the asynchronous nature of JavaScript, this often leads to unexpected results. Let's explore this with a real example.

for (var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000 * i);
}

Enter fullscreen mode Exit fullscreen mode

Expected vs. Actual Output

Expected Output: You might expect this code to print the numbers from 0 to 9, each one second apart.
Actual Output: Instead, when you run this code, you will see 10 printed ten times, each at one-second intervals. This happens because the setTimeout function captures the variable i by reference, not by value. By the time the timeout executes, the loop has already completed, and i is equal to 10.

Fixing the Issue

To achieve the expected behavior, you can use an Immediately Invoked Function Expression (IIFE) or let to create a new scope for each iteration of the loop. Here’s how you can do it:
Using IIFE:

for (var i = 0; i < 10; i++) {
    (function(index) {
        setTimeout(function() {
            console.log(index);
        }, 1000 * index);
    })(i);
}

Enter fullscreen mode Exit fullscreen mode

Using let:


for (let i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000 * i);
}
Enter fullscreen mode Exit fullscreen mode

With IIFE: By wrapping the setTimeout call in an IIFE, you create a new scope for each iteration of the loop. The current value of i is passed as index, which retains its value when the timeout executes.
let: Using let instead of var creates a block-scoped variable. Each iteration of the loop has its own i, so when the timeout executes, it refers to the correct value.

Why is that happening ?

When the for loop runs and setTimeout() gets called sometime in the future (in that case, one second later in the future), the value of the variable i has already been incremented to the end of its range by the for loop.

This is a case where we need to store and access the value of a variable at a given time, before it is modified again. At each iteration loop, we need to “capture” the corresponding copy of i and store it for later use. The good news is that closures are going to help us do just this : create a new closured scope at each iteration of the loop.

We will get back to this example and see how to add a closure to our loop. But before diving deeper inside the workings of closures, let’s take a brief detour and review some important concepts we need to understand well to get a full grasp of closures.

A function is also a variable

A function is a special variable : you can reassign a value to it and pass it to another function as parameters (this is useful in writing asynchronous functions which are passed as callbacks arguments).

The scope of a variable

Variables that are defined outside of a function can be accessed by that function. They can be modified outside of that function as well as by the function itself.

Variables that are defined inside of a function, as well as the arguments passed to a function, are only accessible inside the function.

Passing a variable to a function

When a variable is passed as an argument to a function, the value of the variable is copied inside the argument.

Taking closures by storm

Now that we finished our detour and got back, we are ready to tackle closures.

Let’s consider the following code :

function myFunction(number1) {
    return function add(number2) {
        return number1 + number2;
    };
}

const addFive = myFunction(5);
console.log(addFive(3)); // Output: 8
console.log(addFive(10)); // Output: 15

Enter fullscreen mode Exit fullscreen mode

Let’s dive a little deeper into this function : what’s going on here ?

We define an argument number1 that will take the value of the variable passed to the function.

We define a function inside the function myFunction. In this nested function, the variable number1 is accessible because it has been defined outside of the add function, in its parent function myFunction.

We return the add function, but we are not calling it. What does this mean ?

The myFunction function returns a function : the add function. When I print myFunction to the console, it will display the return result of the function, which is another function (the add function) :

Why is this important ? Because it means that I can assign a value to number1, pass it to myFunction, and this new value of number1 will be stored for later use, when I will be ready to finish the job and call the add function.

And here is the awesome thing about closures : you can write functions with an intermediate state that can capture data from your application, at a very specific moment in time, and you can then use this data at another, later moment in time. Using closures is like adding a “pause” button inside your function. You can go back to it later, when there has been a change in your app (a click event from the user for example), and still be able to retrieve the value of your data before this change in the application happened.

Without closures, JavaScript would simply run all the code and return the last known value of your data, with no way to go back to the moment when the data had the value you now want to use.

That’s what happened in our first for loop example : the setTimeout function kind of arrived late for the war.
The battles of the for loop had already been fought and when the setTimeout cavalry arrived, the i variable had long been assigned 10, the latest value in the loop, and stayed that way until setTimeout arrived.

For better understanding lets recap Closures in JavaScript

Closures are a powerful feature in JavaScript that allow functions to retain access to their outer scope even after that outer function has finished executing. This capability is particularly useful for creating functions with an intermediate state.

Breakdown of the Example

  1. Defining the Outer Function: The function myFunction takes an argument number1. This argument will be stored in the closure.
  2. Creating the Inner Function: Inside myFunction, we define another function called add, which takes a parameter number2. This inner function has access to number1 because it is defined within the scope of myFunction.
  3. Returning the Inner Function: myFunction returns the add function without calling it. This means that add can be invoked later, and it will still have access to number1.
  4. Using the Closure: When we call myFunction(5), it returns the add function with number1 set to 5. We assign this returned function to addFive. Now, when we call addFive(3), it executes the add function, which adds 5 (the value of number1) to 3 (the value of number2), resulting in 8. Similarly, calling addFive(10) results in 15.

Importance of Closures

Intermediate State: Closures allow you to capture the state of variables at a specific moment in time. In the example, number1 retains its value even after myFunction has executed.
Delayed Execution: You can create functions that can be executed later, with the context of their original environment preserved. This is particularly useful in scenarios like event handling or asynchronous programming.
Data Encapsulation: Closures enable you to create private variables that cannot be accessed from outside the function, promoting better data encapsulation and modularity.

Still not convinced or not sure when to use a closure? Let’s take an example and explain how closures work by baking a cake.

Hands on practice Cake Baking

Here’s how we can implement the cake baking functionality using closures:
We are going to use this code that uses a function to bake cakes that have different ingredients and cooking temperatures :

The ovenTemperature() function is a closure, a function inside a function, that can be called at any point in time after the bakeCake() function has been called.

Notice how we have to take two steps to get the whole log in the console ? If your run this code and never call chocolateCake() or carrotCake(), the console will only print :


"chocolate cake : add chocolate to the batter""carrot cake : add carrot to the batter"
Enter fullscreen mode Exit fullscreen mode

You wouldn’t get any error, but the function inside the function, the closure, would not run and the completion of baking the cake would not happen.

Notice also that I can use my bakeCake() function to bake two very different cakes, each one being a separate instance of bakeCake() that will remember its own ingredient argument for later use.

Like in a real recipe, it is not enough to just add the ingredient to the batter, you also have to set the right temperature and baking time to have a perfect cake. And for that, you have to call another function inside the function. As we saw in the earlier example with the add() function, if this inner function isn’t called, the return of the outer function is simply another function, waiting for its time to be called, not a result value (yet).

Here, the bakeCake() function will not return the "ready to bake" line until you also call the ovenTemperature() function with the two arguments : it is on hold until called properly.

You can put the chocolate in the batter, let the batter rest, and take all the time you need to check your recipe book for the right temperature and baking time. It can be an hour later, you may have to call your mother to get advice on it, and the batter with chocolate will still be here, waiting for you to give the final instructions. In other words, anytime you will call chocolateCake(), the ingredient argument, chocolate, will still be incorporated.

So how do I call this inner function when I am ready ?

I have created two instances of the bakeCake()function and assigned them to two different variables : chocolateCake and carrotCake.

Let’s concentrate on the chocolate cake. chocolateCake is a function, and an instance of bakeCake, with a chocolate argument. For chocolateCake to return the "ready to bake" sentence, I just have to call it and pass the arguments needed by the ovenTemperature()function.

This means that chocolateCake will not be fulfilled until we pass a second set of arguments for the closure to be triggered.

Here for simplicity of comprehension we first assigned bakeCake("chocolate") to a variable (chocolateCake), and then passed the second arguments to this variable, which is also a function. But if we already knew all the requirements of the recipe, we could have gone directly :

bakeCake("chocolate")(250, 60);

Enter fullscreen mode Exit fullscreen mode

An inner function always has access to the vars and parameters of its outer function, even after the outer function has returned.

function bakeCake(ingredient) {
    console.log(`${ingredient} cake: add ${ingredient} to the batter`);

    return function ovenTemperature(temp, time) {
        console.log(`Set the oven to ${temp} degrees for ${time} minutes.`);
        console.log(`${ingredient} cake is ready to bake!`);
    };
}

// Creating instances for different cakes
const chocolateCake = bakeCake("chocolate");
const carrotCake = bakeCake("carrot");

// Calling the inner function to complete the baking process
chocolateCake(250, 30); // Output: Set the oven to 250 degrees for 30 minutes. Chocolate cake is ready to bake!
carrotCake(200, 45);    // Output: Set the oven to 200 degrees for 45 minutes. Carrot cake is ready to bake!

Enter fullscreen mode Exit fullscreen mode

Modifying the For Loop Example with Closures

Now, let’s revisit the earlier for loop example with setTimeout and modify it to use closures so that it prints the numbers from 0 to 9 instead of printing 10 ten times.

for (let i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000 * i);
}

Enter fullscreen mode Exit fullscreen mode

Using let:

By using let instead of var, we create a block-scoped variable i. Each iteration of the loop has its own instance of i, which retains its value when the setTimeout function executes.

Closure in Action:

The setTimeout function forms a closure around the current value of i for each iteration. When the timeout executes after the specified delay, it logs the correct value of i (from 0 to 9).

Let’s create a more detailed example that demonstrates how to bake a carrot cake using closures and a modified for loop with let. This example will illustrate how closures can help manage state and allow for delayed execution.

function bakeCake(ingredient) {
    console.log(`${ingredient} cake: add ${ingredient} to the batter`);

    return function ovenTemperature(temp, time) {
        console.log(`Set the oven to ${temp} degrees for ${time} minutes.`);
        console.log(`${ingredient} cake is ready to bake!`);
    };
}

// Creating an instance for carrot cake
const carrotCake = bakeCake("carrot");

// Using a modified for loop with closures
for (let i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(`Baking step ${i + 1}: Add more ingredients for the carrot cake.`);
    }, 1000 * i);
}

// Finalizing the carrot cake baking process
setTimeout(() => {
    carrotCake(180, 45); // Set the oven temperature and time for baking
}, 10000); // Wait until all steps are logged before finalizing


Enter fullscreen mode Exit fullscreen mode

What is happening?

  • Outer Function (bakeCake): The bakeCake function takes an ingredient (in this case, "carrot") and logs a message indicating that the ingredient has been added to the batter. It returns the inner function ovenTemperature, which takes temp (temperature) and time (baking time) as arguments.
  • Creating the Carrot Cake Instance: We create an instance of the carrot cake by calling bakeCake("carrot"), which logs the initial message and returns the ovenTemperature function.
  • Modified For Loop: We use a for loop with let to iterate from 0 to 9. The setTimeout function is called within the loop, creating a closure that captures the current value of i. Each timeout logs a message indicating the baking step for the carrot cake, with a delay of one second between each step.
  • Finalizing the Baking Process: After all baking steps have been logged (after 10 seconds), we call the carrotCake function with the desired temperature (180 degrees) and time (45 minutes) to finalize the baking process. This triggers the inner function, logging the oven settings and confirming that the carrot cake is ready to bake.

Lets add more flavour

Let’s enhance the previous example by adding an array of ingredients for the carrot cake, adding butter, flour, milk, eggs, sugar, and carrots, and demonstrate how to use closures to manage the baking process.

function bakeCake(ingredient) {
    console.log(`${ingredient} cake: add ${ingredient} to the batter`);

    return function ovenTemperature(temp, time) {
        console.log(`Set the oven to ${temp} degrees for ${time} minutes.`);
        console.log(`${ingredient} cake is ready to bake!`);
    };
}

// Array of ingredients for the carrot cake
const ingredients = ['butter', 'flour', 'milk', 'eggs', 'sugar', 'carrots'];

// Creating an instance for carrot cake
const carrotCake = bakeCake("carrot");

// Using a modified for loop with closures to add ingredients
for (let i = 0; i < ingredients.length; i++) {
    setTimeout(function() {
        console.log(`Baking step ${i + 1}: Add ${ingredients[i]} to the batter for the carrot cake.`);
    }, 1000 * i);
}

// Finalizing the carrot cake baking process
setTimeout(() => {
    carrotCake(180, 45); // Set the oven temperature and time for baking
}, 1000 * ingredients.length); // Wait until all steps are logged before finalizing

Enter fullscreen mode Exit fullscreen mode
  • Outer Function (bakeCake): The bakeCake function takes an ingredient (in this case, "carrot") and logs a message indicating that the ingredient has been added to the batter. It returns the inner function ovenTemperature, which takes temp (temperature) and time (baking time) as arguments.
  • Array of Ingredients:
    We define an array called ingredients that contains the necessary ingredients for the carrot cake: ['butter', 'flour', 'milk', 'eggs', 'sugar', 'carrots'].

  • Creating the Carrot Cake Instance:
    We create an instance of the carrot cake by calling bakeCake("carrot"), which logs the initial message and returns the ovenTemperature function.

  • Modified For Loop:
    We use a for loop with let to iterate over the ingredients array. The setTimeout function is called within the loop, creating a closure that captures the current value of i.
    Each timeout logs a message indicating the baking step for the carrot cake, with a delay of one second between each step.

  • Finalizing the Baking Process:
    After all baking steps have been logged (after the total time equal to the number of ingredients), we call the carrotCake function with the desired temperature (180 degrees) and time (45 minutes) to finalize the baking process. This triggers the inner function, logging the oven settings and confirming that the carrot cake is ready to bake.

    Here is a closure with TypeScript

function bakeCake(ingredient: string) {
    console.log(`${ingredient} cake: add ${ingredient} to the batter`);

    // Inner function to set the oven temperature and time
    return function ovenTemperature(temp: number, time: number) {
        console.log(`Set the oven to ${temp}°C for ${time} minutes.`);
        console.log(`${ingredient} cake is ready to bake!`);
    };
}

// Array of ingredients for the carrot cake
const ingredients: string[] = [
    '6 cups grated carrots',
    '1 cup brown sugar',
    '1 cup raisins',
    '4 eggs',
    '1 ½ cups white sugar',
    '1 cup vegetable oil',
    '2 teaspoons vanilla extract',
    '1 cup crushed pineapple, drained',
    '3 cups all-purpose flour',
    '4 teaspoons ground cinnamon',
    '1 ½ teaspoons baking soda',
    '1 teaspoon salt',
    '1 cup chopped walnuts'
];

// Creating an instance for carrot cake
const carrotCake = bakeCake("carrot");

// Using a modified for loop with closures to add ingredients
for (let i = 0; i < ingredients.length; i++) {
    setTimeout(() => {
        console.log(`Baking step ${i + 1}: Add ${ingredients[i]} to the batter for the carrot cake.`);
    }, 1000 * i);
}

// Finalizing the carrot cake baking process
setTimeout(() => {
    carrotCake(175, 45); // Set the oven temperature to 175°C and time to 45 minutes
}, 1000 * ingredients.length); // Wait until all steps are logged before finalizing

Enter fullscreen mode Exit fullscreen mode

Sumup

By leveraging closures, we can create flexible and modular code that allows for delayed execution while retaining access to the necessary context.
Closures on the front-end can also help us achieve a lot of things, like passing parameters on a click event from the user or overcoming the google maps API results limit.
On your journey to becoming an intermediate or advanced JavaScript developer, you will come across closures and now be able to spot them, hopefully use them, and understand some bugs that wouldn’t make sense otherwise.
I hope you enjoyed this introduction to JavaScript closures.

Feel free to comment and like this article.

Happy Coding!

Top comments (0)