DEV Community

Cover image for Understanding Closures in JS (with Examples and Analogies)
itric
itric

Posted on

Understanding Closures in JS (with Examples and Analogies)

[Video version of the post] : https://www.youtube.com/watch?v=UCk7XcAG_Cs

Today, in this post, I’ll be going talk about closures in js, my archnemesis 🤥

The things that I will be covering in this post, are listed in content’s overview:

Content’s Overview:

  1. Starting analogy
  2. What is a Closure?
  3. How Closures Work ?
  4. Examples
  5. 5 analogies to understand
  6. Why Are Closures Useful?
  7. Conclusion

Let’s start this video with an analogy to understand Closures.

Closures:
Imagine you have a secret clubhouse that only you and your friends can access. Inside the clubhouse, you have a bunch of toys and games that you all share. Even if you leave the clubhouse, the toys and games are still there for you and your friends to use whenever you come back. That's kind of like a closure in JavaScript - a function that "remembers" the variables it had access to, even after it's done running.

You might not understand it now, but just keep these lines in mind. You will get clearer picture later.

Now that ,If you have ever encountered a situation in JavaScript where a function seems to remember variables from its outer scope, even after that scope is gone? That's closures behind it!

What is a Closure?

In JavaScript, a closure is a special concept which can be seen in play when a function is defined inside another function, where an inner function (usually anonymous) is able to access variables from its enclosing function's scope, even after the outer function has finished executing (running) or has returned.

The "enclosing function's scope" means the area inside the outer function where its variables and functions are defined. When we talk about an inner function accessing the enclosing function's scope, we mean that the inner function can use the variables and functions from the outer function.

This creates a "closed-over" environment where the inner function remembers the state of the outer function at the time of its creation. In essence, a closure gives you the ability to create functions with "memory" of their originating environment.

I hope you’re getting a clearer picture. In simple words, when you create function inside a function and outer function has returned inner function. It can still access variables from the outer function that contains it.

“Closures remember the outer function scope even after creation time.”

How Closures Work ?

When a function is defined, it has access to:

  1. Its own variables (I mean variables defined inside the function)
  2. Global variables
  3. Variables from the outer function's scope (if the function is nested)

To better understand the third point : “A function's scope refers to the area inside the function where its variables and parameters are defined and accessible.”

The inner function takes / access the variables it needs from the outer function's scope. This is what gives closures their power and flexibility.

Let's break down this concept with some examples that I found.

Example 1: Basic Closure

Consider the following code:

function outerFunction() {
    let outerVariable = 'I am outside!';

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

    return innerFunction;
}

const myClosure = outerFunction();
myClosure();  // Output: 'I am outside!'

Enter fullscreen mode Exit fullscreen mode

In this example:

  • outerFunction defines a variable outerVariable and a function innerFunction.
  • innerFunction can access outerVariable because it is defined within the same scope (area inside a function).
  • outerFunction returns innerFunction, creating a closure.
  • When myClosure is called, it retains access to outerVariable even though outerFunction has completed execution.
function outerFunction() {
  const x = 5;

  function innerFunction() {
    console.log(x); // Can access x from the outer function
  }

  return innerFunction;
}

const myInnerFunction = outerFunction();
myInnerFunction(); // Output: 5
Enter fullscreen mode Exit fullscreen mode

In this example, innerFunction is a closure because it can access the x variable from the outerFunction scope, even after outerFunction has finished executing.

Example 2: Closure with Private Variables

Here's a more complex example:

Closures are often used to create private variables, allowing controlled access through specific functions.

function createCounter() {
    let count = 0;

    return {
        increment: function() {
            count++;
            return count;
        },
        decrement: function() {
            count--;
            return count;
        },
        getCount: function() {
            return count;
        }
    };
}

const counter = createCounter();

console.log(counter.increment());  // Output: 1
console.log(counter.increment());  // Output: 2
console.log(counter.decrement());  // Output: 1
console.log(counter.getCount());   // Output: 1

Enter fullscreen mode Exit fullscreen mode

In this example:

  • createCounter initializes a count variable.
  • It returns an object with methods increment, decrement, and getCount that can modify and access count.
  • count is private and cannot be accessed directly from outside createCounter, but the returned methods form a closure that allows controlled access to count.
function counterFactory(initialValue) {
  let count = initialValue;

  return {
    increment: function() {
      count++;
    },
    decrement: function() {
      count--;
    },
    getCount: function() {
      return count;
    }
  };
}

const counter1 = counterFactory(0);
counter1.increment();
counter1.increment();
console.log(counter1.getCount()); // Output: 2

const counter2 = counterFactory(10);
counter2.decrement();
console.log(counter2.getCount()); // Output: 9
Enter fullscreen mode Exit fullscreen mode

In this example, the counterFactory function returns an object with three methods: increment, decrement, and getCount. These methods have access to the count variable from the counterFactory scope, even after counterFactory has finished executing.

Each call to counterFactory creates a new closure with its own count variable. This allows us to create multiple counters with different initial values.

Example 3: Closure with Parameters

Closures can also capture parameters passed to the outer function. Here's how:

function greet(message) {
    return function(name) {
        console.log(`${message}, ${name}!`);
    };
}

const sayHello = greet('Hello');
sayHello('Alice');  // Output: 'Hello, Alice!'

const sayGoodbye = greet('Goodbye');
sayGoodbye('Bob');  // Output: 'Goodbye, Bob!'

Enter fullscreen mode Exit fullscreen mode

In this example:

  • greet is a function that takes a message parameter and returns another function that takes a name parameter.
  • The inner function forms a closure that captures the message parameter from greet.
  • When sayHello and sayGoodbye are called, they remember the message passed to greet and use it along with the name passed to the inner function.

To help you better digest the concept of closures, here are 5 analogies :

1. The Backpack Analogy

Imagine you are going on a hike and you pack a backpack with some essential items. During your hike, you can take out these items whenever you need them, regardless of where you are on the trail. Similarly, in JavaScript, a function packs its "backpack" with variables from its enclosing scope. Even when the function is executed outside of its original environment, it still has access to those variables.

Code Example:

function createHiker(name) {
    let supplies = ['water', 'food', 'map'];

    return function() {
        console.log(`${name} has these supplies: ${supplies.join(', ')}`);
    };
}

const hiker = createHiker('Alice');
hiker();  // Output: 'Alice has these supplies: water, food, map'

Enter fullscreen mode Exit fullscreen mode

In this analogy, supplies are the items in the backpack, and the function hiker can access these items even after leaving the createHiker "starting point."

Alternative version :

The Backpack Analogy:
Imagine you're packing a backpack for a trip. The backpack represents the outer function, and the items you pack inside it represent the variables and inner functions. When you close the backpack and walk away, the backpack (outer function) has finished its job, but the items inside (the variables and inner functions) are still accessible to you. This is similar to how a closure "closes over" the variables it needs from the outer function, even after the outer function has finished executing.

2. The Cookie Jar Analogy

Think of a child reaching into a cookie jar that is placed on a high shelf by a parent. The child remembers where the jar is and can grab cookies whenever they want. In JavaScript, the parent function sets up a "cookie jar" (variables), and the inner function (child) can access the jar even after the parent function is no longer in the picture.

Code Example:

function createCookieJar() {
    let cookies = ['chocolate chip', 'oatmeal raisin', 'peanut butter'];

    return function() {
        return cookies;
    };
}

const getCookies = createCookieJar();
console.log(getCookies());  // Output: ['chocolate chip', 'oatmeal raisin', 'peanut butter']

Enter fullscreen mode Exit fullscreen mode

Here, the inner function getCookies remembers the cookies variable (the cookie jar) and can access it anytime, even though createCookieJar has finished executing.

3. The Library Card Analogy

Imagine you have a library card that gives you access to the library's resources. Even if you leave the library, you can come back and use your card to check out books. Similarly, in JavaScript, an inner function gets a "library card" to access the outer function's variables and can use this access even after the outer function has returned.

Code Example:

function library() {
    let books = ['Moby Dick', 'Hamlet', '1984'];

    return function() {
        console.log(`Available books: ${books.join(', ')}`);
    };
}

const accessLibrary = library();
accessLibrary();  // Output: 'Available books: Moby Dick, Hamlet, 1984'

Enter fullscreen mode Exit fullscreen mode

In this analogy, the books are the library's resources, and the inner function accessLibrary retains the "library card" to access these resources even after the library function has executed.

4 . The Mailing Package Analogy:

Suppose you need to mail a package to a friend. The process of packing the box and sealing it represents the outer function, and the contents of the box represent the variables and inner functions. Once the package is sealed, you can write the address on it and send it off. The address function (inner function) has access to the contents of the package (variables from the outer function), even though the packing process (outer function) is complete.

5 . The Nested Doll Analogy:

Imagine a set of Russian nesting dolls, where each doll contains a smaller doll inside it. The outer doll represents the outer function, and the inner dolls represent the variables and inner functions. When you open the outer doll, you can access the inner dolls, just like how a closure allows the inner function to access the variables from the outer function's scope.

Why Are Closures Useful?

Closures are incredibly useful in JavaScript for several reasons:

  1. Data privacy and Encapsulation: They allow you to create private variables that can't be accessed directly from outside the function, promoting data encapsulation.
  2. Partial application and currying: Closures can be used to create functions that remember some of their arguments.
  3. Functional Programming: Closures are a key feature in functional programming, enabling higher-order functions, currying, and other advanced functional techniques.
  4. Memoization: Closures can be used to cache the results of expensive function calls and return the cached result when the same inputs occur again.
  5. Function Factories: Closures can be used to generate functions with pre-configured settings, creating a sort of function factory.
  6. Event Handling and callbacks: They are often used in event handlers and callback functions, maintaining access to variables even when the context changes.
  7. Event Listeners: Closures are commonly used in event listeners to capture the state of variables at the time the listener is attached. And to maintain access to variables from the surrounding scope, even after the outer function has finished executing. This is particularly useful for creating more dynamic and flexible event handling.

Conclusion

Closures might seem complex at first, but with practice, they become a valuable tool in your JavaScript development arsenal. By understanding how inner functions access variables from outer scopes, you can create versatile and well-structured code.

Closures are a powerful feature of JavaScript that enable functions to have private variables and retain access to their originating scope. By understanding and leveraging closures, you can write more modular, encapsulated, and expressive JavaScript code. Whether you're creating private variables, building higher-order functions, or handling events.

So, the next time you encounter a function with a surprising memory, remember, it might just be a closure in play!

Happy coding!

Top comments (0)