DEV Community

Cover image for JavaScript Closures
Mihlali Jordan
Mihlali Jordan

Posted on

JavaScript Closures

In today's blog post, I am going to attempt to explain a concept I have grown to love and appreciate, one that is also commonly misunderstood and somewhat complex: JavaScript Closures.

Before we go any further, there is a term that I am going to use excessively throughout this article which is very important to understand. That term is execution context.

We can think of execution context as the environment the current code is being evaluated in.

So for example global execution context would refer to the global environment – the default environment where your code first runs whenever you execute a program. Local execution context would refer to the local context of a function. Variables declared in the global execution context are referred to as global variables and variables declared within the local execution context of a given function are referred to as local variables as in their are local to the context and scope of that particular function.

The one last term I would like to explain before we jump into things is the Call Stack. This is basically a mechanism JavaScript uses to keep track of where it is in the order of execution within a program.

What happens when we invoke a function?

It is very important to understand what is going on under the hood every time we run a function. Let's briefly go over this:

  1. A brand new execution context is created – a local execution context and inside of this context all local variables of that function will be stored.
  2. The local execution context of the function is added at the very top of the call stack.

What happens when the function terminates?

A function is terminated when it encounters either a return statement or a closing bracket }. So what happens under the hood when a function encounters either one of these?

  1. The local execution context of the function is removed from the call stack.
  2. If the function has any return values, it will return them out into the context within which it was called. If the function was called in the global context, the value will be returned out to the global context. If the function was called within the local context of another function, the value will be returned out to the local context of the function within which the function returning out the value was called. If the function does not have any return values, it returns out undefined
  3. Lastly, the local execution context of that function is destroyed. This means that it no longer exists in memory and all the variables that were declared within that local execution context are destroyed and removed from memory as well. They are no longer available.

What does this look like in practice?

Let's take a look at the function below.

let x = 5
function squared(a) {
  let squaredValue = a * a
  return squaredValue
}
let y = squared(x)
console.log(y)
Enter fullscreen mode Exit fullscreen mode

So this looks pretty simple, but there is quite a lot going on here. Let's go through this line by line.

  1. Line 1 – First we declare a variable x in the global execution context and assign it the value 5. This variable is stored in global memory.
  2. Still in the global execution context, we then declare the variable squared and assign to it a function definition. So what has happened here is that the code inside the function block is stored in memory inside the variable squared. At this point in time, the function is not invoked and the code inside the function block does not run – it is only stored in global memory.
  3. Line 7 – We then jump down to right after the function declaration and declare the variable y and assign to it the result of the invocation of the function squared with the argument x passed into it. Before squared is invoked, the variable x is uninitialised.
  4. The function squared is invoked.
  5. A brand new local execution context is created – we shall call this "squared execution context".
  6. We head back up to the function declaration and into the function block. NOTE: Javascript does not go back up to the code. The thread of execution is top down, it never goes up and in this case does not jump from line 6 back up to line 2 – it does not have to because when it saved squared in the beginning of the code, it saved all of the code in memory which is where it grabs the code from to start executing it.
  7. We head into the function block in lines 2 and 3 – now you might be tempted to say that the first thing that happens is that we declare the variable squaredValue and assign to it the result of the expression a * a but there is a step before that. Before this, JavaScript looks for any arguments passed in. Because this function accepts arguments, and has the parameter a, a new variable a is declared in the local execution context and assigned to it is the argument that was passed in when the function was invoked which is x. JavaScript will look in global memory for x and find that the value is assigned to it is 5 and then assign the value 5 to a. Note that a is stored in the memory of the "squared execution context".
  8. The value is returned out to global context and is stored in variable y.
  9. The "squared execution context" is removed from the call stack and all the variables that were defined in the "squared execution context" are erased as well.
  10. Line 7 – We then log the value of y out onto the console.

JavaScript: A lexically scoped language

We're getting there. I know this might seem like a lot but all of this is crucial to understanding closures. The next concept I want to touch on is Lexical Scope. Well firstly, what is scope 🤔?

Scope is essentially a set of rules that define what data is available to you at any given line in the running of your code.
JavaScript uses lexical (static) scoping. Let's take a function for example; wherever that function is saved determines what data the function will have access to whenever it runs. Let's take a quick detour and look at this quick example.

let num = 3
function foo(x) {
  return x + num
}
Enter fullscreen mode Exit fullscreen mode

Where is foo declared? In global context and as such has access to all the variables in the global context.

function foo(){
  let num2 = 10
  return num2 * num2
}

function doSomething(){
  return num2 + 10
}

console.log(foo())
console.log(doSomething())
Enter fullscreen mode Exit fullscreen mode

What do you think will happen when we try run the last line of this code? We will get a reference error of "num2 is not defined". We get this error because num2 is out of scope and not within the scope of doSomething. JavaScript first looks in the function's local context for a variable and if it's not there, looks further up in its calling context and it if the variable is not found there still will continue further up until it reaches the global execution context. If the variable is not found in the global execution context, JavaScript will assign the value undefined to the variable.

In our example above, JavaScript looks inside of the local execution context of doSomething for the variable num2 which it does not find and so it jumps up to the global context and still does not find the variable there and so it assigns the value undefined to it.

So lexical scope just means that a function has access to variables that are defined in its calling context. Where the function is defined determines what variables it has access to.

Now there are quite a number of caveats when it comes to scope which are beyond the scope of this article (do you get it? 😉😂). Maybe in future, I will post another article that deals specifically with scope in JavaScript.

A function returning another function? 💭

We are almost there! This is the final piece of the puzzle before we get to the beauty that are closures.

So, in JavaScript, it is very possible for us to return functions from other functions. Let's take a look at an example.

function firstFunction(){
  function secondFunction(x) {
    return x * x
  }
  return secondFunction
}
const createdFunction = firstFunction()
const finalResult = createdFunction(10)
console.log(finalResult)
Enter fullscreen mode Exit fullscreen mode

Let's dive deeper into what exactly is going on in the above code.

  1. In line 1 we have our first function declaration. We declare the variable firstFunction and we assign a function definition to it which is essentially the body of the function.
  2. We head down to line 7 where we declare the variable createdFunction and assign to it the result of the invocation of firstFunction. Until firstFunction is invoked, createdFunction is uninitialised.
  3. We invoke firstFunction and create a brand new local execution context which we will refer to as first local execution context. When this invocation takes place, first local execution context is added to the top of the call stack.
  4. We are now back at line 2, inside the first local execution context where we declare the variable secondFunction and assign to it a function definition. This variable is stored inside the memory of first local execution context.
  5. In line 5, we return out the value of secondFunction to the global execution context where it is stored inside the variable createdFunction.
  6. The first local execution context is removed from the call stack and it is destroyed and so are all of the variables that were defined in and stored in the memory of the first local execution context.
  7. In the final line, line 8, we declare the variable finalResult in the global execution context and assign to it the result of the invocation of createdFunction(10).
  8. We invoke createdFunction with the value 10 passed in as an argument.
  9. Upon invocation, a brand new local execution context is created, we will call this second local execution context.
  10. Second local execution context is added onto the top of the call stack.
  11. The first thing JavaScript will do once the function is invoked, before even heading to the function body is check whether the function has parameters and whether arguments were passed in so the values of the respective arguments can be assigned to the parameters. With our example, there is a parameter: x. JavaScript will declare a variable x in the second local execution context and assign to it the value that was passed in as an argument which in this case is 10. So right now, in the memory of second local execution context is variable x with the value 10.
  12. In line 3 of the code the result of the expression x * x is returned out. Where is this value returned out to? You might think that it is returned out to first local execution context. It might look like that because the secondFunction is nested inside of firstFunction but that is not actually the case. Remember that at this point first local execution context does not exist and createdFunction is not being called inside of firstFunction. This would be a good time to remind you that a function will return a value out into its calling context. createdFunction was called in global context and as such the result of the earlier expression (x * x) will be returned out into the global execution context and stored in the variable finalResult.
  13. Once the value is returned out, second local execution context is popped off the call stack and is erased along with all of the variables that were defined inside of it.
  14. We then finally log to the console the value of finalResult is 100.

Now you might be wondering why I went through all of these steps in so much detail. The primary reason is to place emphasis on understanding what is going on in each line of code. The second reason is so that you have it engrained in your understanding that a brand new execution context is created every single time a function runs and that the newly created execution context is destroyed and erased when the function terminates along with all of the variables that were defined within that context. Lastly, I wanted to show you that functions can return functions – this is the fundamental pillar of the concept of closures.

JavaScript Closures

You made it! You're finally here. Now that we have laid down a solid foundation we can dig into what closures are and why they are such a wonderful feature in JavaScript.

Before we look at an example I want to quickly provide a brief explanation of what a closure is. You can almost think of it as a function's own little piece of memory. Closures are essentially a function's way of keeping memory of all the data that was inside the surrounding context within which the function was initially defined. For those looking for a technical definition:

A closure is a collection of all the variables that were in scope at the time of the function's creation.
The closure contains all of the variables that are in scope at the time the function is created. Think of closure as a backpack that the function carries with it wherever it goes.

This might not make sense now so let's take a look at an example.

function doSomething(){
  let num = 1
  function doubleNum(){
    num++
    console.log(num)
  }
  return doubleNum
}
const doSomethingElse = doSomething()
doSomethingElse()
doSomethingElse()
const doSomethingBetter = doSomething()
doSomethingBetter()
doSomethingBetter()
Enter fullscreen mode Exit fullscreen mode

Let's dive into this function line by line.

  1. In line 1 we declare the variable doSomething and assign to it a function definition inside of the global execution context.
  2. We jump down to line 9 and declare the variable doSomethingElse and assign to it the result of the invocation of doSomething. Until doSomething is invoked, doSomethingElse is uninitialised.
  3. doSomething is invoked and we create a brand new local execution context which we will refer to as doSomething execution context.
  4. doSomething execution context is added to the top of the call stack.
  5. We jump to line 2 inside the body of doSomething.
  6. We declare the variable num and assign to it the value 1. This variable is stored inside the memory of doSomething execution context.
  7. In line 3, still inside doSomething execution context we declare the variable doubleNum and assign to it a function definition. This is where it gets exciting!! This is where we see the beauty of closures. When doubleNum is defined, what also happens is that JavaScript creates a bind to the local variable environment through a hidden scope property on the function doubleNum. This means that when we return out doubleNum, attached to it is all of the local data. All the variables in scope at the time doubleNum is defined are attached to doubleNum. doubleNum has closure which contains inside of it all the variables that are in the context of which it is defined. So in the case of the above code, inside the closure of doubleNum are all of the variables inside of doSomething execution context which is basically just num.
  8. In line 7, we return out doubleNum. Not only do we return out the function definition, we also return out the closure – all of the variables in doSomething execution context. So the function definition assigned to doubleNum and its closure is returned out to the global context and stored in the variable doSomethingElse.
  9. After we return out doubleNum, the doSomething execution context is removed from the call stack and destroyed along with all of the variables that were defined inside of doSomething execution context. To further emphasise, at this point, doubleNum and num are erased as the doSomething execution context no longer exists.
  10. We are now at line 10 and we invoke doSomethingElse.
  11. A brand new execution context is created which we will call the doSomethingElse execution context.
  12. doSomethingElse execution context is added onto the call stack.
  13. We are now at line 4 inside the function body of doubleNum. Remember, we are only jumping back up there so we can see what code is being executed when doSomethingElse is invoked. JavaScript has the function definition stored in memory and does not need to go back up in the execution thread. In line 4, we increment num by 1. Before JavaScript performs the increment operation, it needs to get a value for num. So where will it look first? You guessed it – in the closure. Stored in the closure attached to the function definition that was assigned to doSomethingElse, is the variable num which has a value of 1. JavaScript then increments num by 1. This value is updated within the closure of doSomethingElse. This means that now num is 2.
  14. We then console log out the value which is 2.
  15. Back at line 11, we invoke doSomethingElse again and repeat steps 11 - 14. The are two differences this time however. The first difference is that we are no longer dealing with doSomethingElse execution context, we are dealing with an entirely brand new execution context which we can refer to as doSomethingElse2 execution context. The second difference is that in step 13 when JavaScript looks to increment num, it looks inside the closure and finds that the value is 2 (as a result of the first invocation of doSomethingElse) and the result of the operation will be 3.
  16. With doSomethingElse2 execution context removed from the call stack, we are now at line 12 where we declare the variable doSomethingBetter and assign to it the result of the invocation of doSomething.
  17. We then repeat steps 3-9 with two key differences. The first being that again, a brand new execution context is created and we can call this doSomething2 execution context. doSomething execution context no longer exists. The second key difference to note is that a brand new closure is created and returned out to the variable doSomethingBetter along with the function definition. Remember this as it will prove important in the next couple of steps – doSomethingElse and doSomethingBetter have different closures.
  18. Now at line 13, we invoke the function doSomethingBetter. We repeat steps 11 - 14 with two important differences to remember. The first being that a brand new execution context is created which we will call doSomethingBetter execution context. The second important thing to remember is the value of num. You might be tempted to say that when JavaScript looks inside the closure for the value of num it will be 3 because of the previous invocations of doSomethingElse. This is not the case. Because doSomethingElse and doSomethingBetter have different closures, the value of num initially in the closure of doSomethingBetter is 1 as this was the value of num when the function definition was returned out to doSomethingBetter. Having said that, the value of num inside the closure of doSomethingBetter will be incremented by 1 resulting in 3.
  19. Once the operation has taken place and doSomethingBetter execution context is removed from the call stack and destroyed, we go to the final line, line 14 where we invoke doSomethingBetter a second time.
  20. A brand new execution context is created we run through steps 11 - 14 again taking into account the fact that the value of num inside the closure of doSomethingBetter is now 2 and that we are dealing with a brand new execution context and that doSomethingBetter execution context does not exist at this stage.

Now this might seem like a mouthful and quite a lot to digest but there are a few things I wanted to highlight in this illustration. The first is that every time a function is returned out and stored in a variable, the function definition of that function has attached to it its own unique closure. We see this with doSomethingElse and doSomethingBetter – the variable num in the closures of these functions are not the same. Each individual function gets its own private closure with data inside from the running of the function doSomething, each has their own compartmentalised pocket of data. The second point I wanted to drive home is that a new execution context is created every single time a function is invoked and that execution context is destroyed when the function is terminated.

What's the point?

At this stage you might still be wondering why and how this could be remotely useful? Well the beauty with closures is that they give our functions persistent memories and an entirely new toolkit for writing professional code. They give us the toolset to create helper functions like once and memoize. Iterators and generators use lexical scoping and closure to achieve patterns for handling data in JS. Closures help us in Asynchronous JavaScript – callbacks and promises rely on closure to persist state in an asynchronous environment.

Conclusion

If this all gets to complex for you, think of closure as a backpack that a given function carries around with it everywhere it goes. When it gets returned out of another function it gets returned out carrying its backpack which has inside of it all of the variables that were in the local environment where the function would have been defined.

I would like to give credit and a shoutout to Will Sentance for explaining this concept so wonderfully in the JS Hard Parts Series.

If you enjoyed reading this and you found it useful, please do not forget to share! 🚀 💥

Top comments (0)