Guide to JavaScript Closures

Introduction

Closures are a somewhat abstract concept of the JavaScript language and sneak into the compiler-side of programming. However, understanding how JavaScript interprets functions, nested functions, scopes and lexical environments is imperative to harnessing its full potential.

In this article, we will try to demystify said concepts and provide a simple guide to JavaScript Closures.

What is a Closure?

First let's take a look at the official MDN definition of closure:

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 simpler terms, a closure is a function that has access to an outer function's scope. To understand this, let's take a look at how scopes work in JavaScript.

Scope in JavaScript

Scope determines which variables are visible or can be referenced in a given context. Scope is broadly divided into two types - Global Scope and Local Scope:

  • Global Scope - variables defined outside a function. Variables in this scope can be accessed and altered from anywhere in the program, hence the name "global".

  • Local Scope - variables defined inside a function. These variables are specific to the function in which they are defined, hence named "local".

Let's take a look at a global and local variable in JavaScript:

let name = "Joe";

function hello(){
    let message = "Hello";
    console.log(message + " " +name);
}

In the example above, scope of name is global, i.e. it can be accessed anywhere. On the other hand, message is defined inside a function, its scope is local to the hello() function.

JavaScript uses Lexical Scoping when it comes to function scopes. Meaning that the scope of a variable is defined by the position of its definition in the source code. This lets us reference global variables within smaller scopes. A local variable can use a global variable, but vice-versa isn't possible.

On

function outer(){
    let x = 10;
    
    function inner() {
        let y = 20;
        console.log(x);
    }
    
    inner();
    console.log(y)
}

outer();

This code results in:

10
error: Uncaught ReferenceError: y is not defined

The inner() function can reference x since it's defined in the outer() function. However, the console.log(y) statement in the outer() function cannot reference the y variable because it's defined in the inner() function's scope.

Additionally, in this scenario:

let x = 10;

function func1(){
   console.log(x);
}

function func2() {
  let x = 20;
  func1();
}

func2();

The output will be:

10

When we call func1() from within func2(), we've got a locally scoped variable x. However, this variable is totally irrelevant to func1() as it's not accessible in func1().

Thus, func1() checks if there's a global variable with that identifier available, and uses it, resulting in the value of 10.

Closures Under the Hood

A closure is a function which has access to it's parent's variables even after the outer function has returned. In other words, a closure has three scopes:

  • Local Scope - Access to variables in its own scope
  • Parent Function's scope - Access to variables within its parent
  • Global Scope - Access to global variables

Let's take a look at a closure at work, by making a function that returns another function:

function outer() {
    let x = 3
    return function inner(y) {
        return x*y
    }
}

let multiplyByThree = outer();

console.log(multiplyByThree(2));

This results in:

6

If we do a:

console.log(multiplyByThree);

We're greeted with:

function inner(y) { return x * y; }

Let's go through the code step-by-step to see what is happening under the hood:

  1. The outer() function is defined in global scope.
  2. outer() is invoked, and it returns a function that is assigned to multiplyByThree.
    1. New execution context is created for outer().
      • Variable x is set to 3.
    2. Returns a function named inner().
    3. The reference to inner() is assigned to multiplyByThree.
    4. As the outer function finishes execution, all the variables within its scope are deleted.
  3. Result of the function call multiplyByThree(2) is logged to the console.
    1. inner()is invoked with 2 as the argument. So, y is set to 2.
    2. As inner() preserves the scope chain of its parent function, at the time of execution it will still have access to the value of x.
    3. It returns 6 which gets logged to the console.

In conclusion, even after the outer function ceases to exist, the inner function has access to the variables defined in the scope of the outer function.

Visualizing Closures

Closures can be visualized through the developer console:

function outer() {
    let x = 3
    return function inner(y) {
        return x*y
    }
}

let multiplyByThree = outside();
console.dir(multiplyByThree);

By executing the code above in the developer console, we can see we have access to the context of inner(y). Upon closer inspection, we can see that part its context is a [[Scopes]] array, which contains all three scopes we were talking about.

Lo and behold, the array of scopes contains its parent function's scope, which contains x = 3:

Closure element in developer console

Common Use Cases

Closures are useful because they help us cluster data with functions that operate on that data. This might ring a bell to some of you who are familiar with Object-Oriented Programming (OOP). As a result, we can use closures anywhere we might use an object.

Another major use-case of closures is when we need our variables to be private, as variables defined in the scope of a closure is off-limits to the functions outside of it. At the same time, closures have access to variables in its scope chain.

Let's look at the following example to understand this better:

const balance = (function() {
    let privateBalance = 0;

    return {
        increment: function(value){
            privateBalance += value;
            return privateBalance;
        },
        decrement: function(value){
            privateBalance -= value;
            return privateBalance;
        },
        show: function(){
            return privateBalance;
        }
    }
})()

console.log(balance.show()); // 0
console.log(balance.increment(500)); // 500
console.log(balance.decrement(200)); // 300

In this example, we have defined a constant variable balance and set it as the return value of our anonymous function. Notice that privateBalance can only be changed by calling the methods on balance.

Conclusion

Although closures are a fairly niche concept in JavaScript, they're an important tool in good JavaScript developer's toolkit. They can be used to elegantly implement solutions which would otherwise be a tall order.

In this article, we have first learned a bit about scopes and how they are implemented in JavaScript. We then used this knowledge to understand how closures work under the hood and how to use them.

Author image
India Website
Hey, I am a full-stack web developer located in India. I am a curious person who is always trying to wrap my head around new technologies. In my free time, I read novels and play with my dog!