Demystifying Closure in Javascript

Closure is a scary monster in Javascript land. It can feel like an enigma, a secret that is only known by wizards and lab coat wearing scientists 🥼🧪. At least that's how I used to feel about it. Turns out, it's not all that spooky, as I will hopefully demonstrate in this short post.

What is Closure?

Kyle Simpson, author of You Don’t Know JS puts it like this:

Closure is when a a function "remembers" it's lexical scope even when that function is executed outside that lexical scope.

In other words, we can't understand Closure without first understanding what "lexical scope" is. The good news is that the term "lexical scope" is just another scary sounding word that we can demystify.

What is Lexical Scope?

Lexical scope is the surrounding environment that a variable or function is defined in. In Javascript, a variable or function is visible to the executing code if it is in the current "lexical" scope of the enclosing function.

Let's take a look at this example:

function outer() {
    let counter = 0

    function incrementCounter() {
        counter++
    }

    return incrementCounter
}

const myNewFunction = outer();
// myNewFunction === incrementCounter

myNewFunction()

This is a common pattern in Javascript, storing a function inside another function. The function outer is a "higher order" function because it is returning another function incrementCounter.

Intuitively, when looking at the code inside incrementCounter we'd presume that Javascript has no knowledge of the variable counter. This is because it is not defined inside incrementCounter. What gives? The Javascript parser initially does look inside incrementCounter for the variable counter. The cool part is that if it doesn't find it inside its immediate surroundings, it will then look up the "chain" and find counter defined in the enclosing function, in this case outer.

Because Javascript is lexically scoped, where you define your function determines what variables it has access to when it is executed. Since incrementCounter is defined inside of outer it has access to variables within that enclosing scope.

After we define outer, we assign it's returned value to myNewFunction. As you can see, the returned value of outer is the function incrementCounter. To be clear, we are not returning the execution of the function, but the function definition. In other words, the contents of function:

function incrementCounter() {
   counter++
}

Therefore, when we eventually call or "execute" myNewFunction, we are really saying "go execute the incrementCounter functionality." Through the power of closure, when we execute myNewFunction, we have access to the surrounding data in it's enclosed environment. In our example, that is the variable counter. Since counter is not living inside incrementCounter itself, it's value persists between function calls, making it a live store of data. If counter were to be defined inside incrementCounter, there's no way of holding onto it's value and continue updating it. Here's how that would look:

function outer() {
    function incrementCounter() {
        let counter = 0
        counter++
    }
    return incrementCounter
}

let myNewFunction = outer();

myNewFunction()

In this example, after executing myNewFunction(), the counter would be assigned the value of 0, then we'd increment it by 1. On the second call, the same thing would happen. The counter would be assigned the value 0 and then be incremented by 1. Let's take a look at the original example again:

function outer() {
    let counter = 0
    function incrementCounter() {
        counter++
    }
    return incrementCounter
}

let myNewFunction = outer();

myNewFunction()

Now, the counter will be preserved on each subsequent execution of myNewFunction. On the initial call, counter is assigned the value of 0 as it was set in the enclosing function outer. When we invoke myNewFunction we are simply incrementing the counter by 1. The next time myNewFunction is called, the value of counter is what we incremented it to on the first call, 1. Again, we go ahead and increment the counter by 1, and it's value becomes 2. On the subsequent call the value will be 2, then we increment it to 3 and so on.

This example clearly demonstrates the power of closure. Closure gives our functions memory. With closure, we empower our functions by giving them the ability to "remember" the state of our data. We can keep a live store of data that can be updated each time the function is called.

Closure is used everywhere in Javascript. For my fellow react developers out there, check out this deep dive into how Hooks work under the hood. Spoiler alert: Closure.

Further your learning

I want to give proper credit to the course platform Frontend Masters for inspiring this post. More specifically, the workshop Javascript: The Hard Parts led by instructor Will Sentance from Codesmith. Here I've attempted to distill Will's lesson on Closure into a short, readable format. For the real deal, you should absolutely check out his course as well as the many other great ones on Frontend Masters.