Closures

A beginners introduction

MDN:

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 [1].

If you are confused or underwhelmed by this explanation, you are in good company. Although succinct and correct, many technical definitions of closures make them seem either enigmatic and out of reach, or overhyped--which is not the case.

Explaining closures is analogous to describing air: it is all around us, we need and use it to survive, but when you try to get down to the nitty gritty of what it really is (primarily nitrogen, oxygen, argon, and carbon dioxide) our most accurate definitions are not always the most accessible or practical.

In a similar vein, closures are not rare or mysterious, in fact, they are almost everywhere in JavaScript programs, and we depend on them so heavily that I am confident that you have used them in your code!

In this article:

  • Defining closures and common use cases
  • Examples of private variables to help build an intuitive understanding of closures

Closure definitions

As we can infer from the MDN definition, a closure is the scope created when a function is declared that allows the function to access and manipulate variables that are external to the function [2], [3]. When performing such operations, the JavaScript interpreter needs to know about the function itself and have access to any other data that it returns, or depends upon from the surrounding environment, and closures provide the magic to make this possible.

But don’t take my word for it; just as a picture is worth a thousand words, I believe a code example of a closure is worth a thousand definitions.

Building an intuitive understanding

Before making this harder than it needs to be, let’s start with the basics. The lexical scope in which we create our data determines where and how all identifiers are declared and predicts how they will be looked up during execution, as noted in [1],[4],[5].

In the example below, we have a pure function that is fully self-contained in the sense that it depends on the arguments passed to it and internal data. When executed, our function gets pushed onto the call stack where the internal data is only kept until it is removed from the stack.

function add(a, b, c) {
   return a + b + c
}
let sum = add(1, 2, 3)

But when a function references data outside its scope we create an open expression that references other free variables—variables that are locally declared or passed as a parameter—throughout the environment.

For the interpreter to call such a function and know the value of the free variable(s), it creates a closure to store this data in memory for later access. Closures are functions combined with its outer state or lexical environment.

function outer(){
  let outerData = "outside the inner function";
  function inner(){
     return outerData
}
  return inner;
}

let test = outer()
test() //=> 'outside the function'

In this example, we see that the function inner() has lexical scope access to the variable outerData, located in the inner scope of the parent function outer(). This is the result of the function inner() having closure over the scope of outer().

Instead of erasing outerData from memory after the outer() function executes, we are instead able to be access it at a later point in time because our inner() function is still actively using it. By passing the function inner() as a return value and returning the object inner() references, or the function definition, we can assign it to a variable for future use.

We see this in our example, after the outer() function definition. We use the keyword let to create a variable named test and assign it to the value returned after executing the function outer(). By storing the return value of outer() in a variable, we can later invoke test, which in turn invokes our function inner(), since test is a reference to inner. This reference is the closure.

When we invoke test(), we are calling the function inner(), which remembers the environment in which it was created, giving us access to the lexical scope at the time in which inner() was defined.

This might seem more confusing than groundbreaking, however a lot of popular JavaScript APIs and other powerful functionality is based upon or leverage this ability made possible by closures.

Common use cases

Closures are necessary in JavaScript to encapsulate private data and help prevent outside exposure or manipulation. They help achieve these ends by defining an outer function that contains the private data and an inner function that operates on the data.

function counter(){
    var count = 0;

    return function(){
        count++;
        return count;
    }
}

let counter1 = counter();
counter1(); //1
counter1(); //2

let counter2 = counter();
counter2(); //1
counter2(); //2
counter1(); //3 this is not affected by counter()2;
count; //ReferenceError: count is not defined -- because it is private!

This self-contained structure prevents leaking of the inner data to the surrounding outer environment since the inner function has access to data defined in the outer function’s scope, but the outer function does not.

function team(){
    let roster = ["Harden", "Embiid", "Maxey", "Tobias", "P.J."];
    return {
        getRoster: function(){
            return roster.slice();
        },
        addPlayer: function(player){
            roster.push(player)
            return roster.slice()
        }
    }
}

Building upon our understanding of closures and private variables, our function team() returns an object with the key values getRoster and addPlayer, which allow us to see our roster variables and add players to the array. Perhaps you are a little disgruntled and no longer ‘Trust the Process’ and want to get rid of one of these amazing NBA superstars using the pop() method. Thankfully, due to closures, this is not possible.

TL;DR:

  • Closures are a combination of a function and the environment within which that function was declared
  • An example of a closures is when an inner function makes use of variables defined in an outer function that has returned
  • Closures do not remember everything from an outer function
  • Closures only remember variables used in the inner function
  • A common use case of closure in JavaScript is to create a private variable

References

[1] “Closures” MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaSc.. (August 17, 2023).

[2] V. Antani, S. Timms, N. Prusty, JavaScript: Moving to ES2015.Packt Publishing. [Online]. Available: oreilly.com/library/view/javascript-moving-... Accessed: August 17, 2023.

[3] “JavaScript Closures” JavaScript Tutorial. javascripttutorial.net/javascript-closure (August 17, 2023).

[4] “JavaScript Closures” W3Schools. w3schools.com/js/js_function_closures.asp (August 17, 2023).

[5] “Closures – Beau Teaches JavaScript” YouTube. youtube.com/watch?v=1JsJx1x35c0 (August 17, 2023).

[6] K. Simpson, You Don’t Know JS. O’Reilly Media, Inc. [Online]. Available: oreilly.com/library/view/you-dont-know/9781... Accessed: August 17, 2023.