Closures
There is a complex feature of JavaScript functions that many developers fail to understand fully. And what I'm talking about is something called closures. However, I believe that with the right explanation, it's not that hard. Especially when you already understood everything you learned before in this course, such as the execution context, the call stack, and the scope chain, because closures bring all of these concepts together in a beautiful, almost magical way.
To show you what closures are, let's start by creating a function called secureBooking
.
const secureBooking = function () {
// ...
}
Before moving on, one thing that you have to keep in mind about closures is that, a closure is not a feature we explicitly use. We don't create closures manually like we create a new array or a new function. A closure simply happens automatically in certain situations, and we just need to recognize those situations. Hence, that's what we're going to do in our secureBooking
example. We will create one of those situations so that we can then take a look at a closure.
Let's complete the secureBooking
function.
const secureBooking = function () {
let passengerCount = 0;
// Function returning another function. Hope you rember! :)
return function () {
passengerCount++;
console.log(`${passengerCount} passengers.`);
}
}
Let's now call the secureBooking
function and store the result in a variable called booker
.
const booker = secureBooking();
Let's now analyze what happens when the above highlightrd line of code is executed using all the concepts that we already know about.
The above image describes exactly the code we wrote. Before we start running the secureBooking
function, our code is running in the global execution context. And in there, we currently only have the secureBooking
function. Hence, we can also say that the global scope now contains secureBooking
.
Now, when the secureBooking
function is actually executed, a new execution context is created and added to the execution stack.
Remember that each execution context has a variable environment which contains all it's local variables. In this case it only contains one variable, passengerCount
set to 0.
This variable environment is also the scope of the function. Hence, the scope chain of this execution context looks like this:
As you can see, passengerCount
is in the local scope, but of course this scope (secureBooking
) also gets access to all the variables of the parent scope (the global scope). The next line of our secureBooking
function is the return statement. This return statement returns a function which is then stored in a variable called booker
.
Hence, the global scope now also contains the booker
variable. And now what else happens when the secureBooking
function returns? Well, its execution context pops off the stack and disappears.
The secureBooking
function is done. Its execution is complete, and it's gone now. That's important to be aware of and to keep in mind. For now that's all we did and nothing should be new up till this point. All we did was to analyze the call stack and scope chain as we call the secureBooking
function. And this is going to be important to later on understand the closure.
As of yet, we haven't seen the closure yet. All we did was use the knowledge we already have to understand how the booker
function was created because that will be important for the next step.
Now that we understand how the booker
function was created, let's now head back and execute the booker
function to see the closure in action.
booker();
booker();
booker();
From the above result, you can see that we get one, two, and three passengers. This actually means that the booker
function was, in fact, able to increment the passengerCount
variable to one, two, and three. But now, if we think about this, then how is this even possible? How can the booker
update the passengerCount
variable that's defined in the secureBooking
function that has already finished executing.
As mentionned, the secureBooking
function execution is over and it's gone. That is, its execution context in no longer on the stack as we saw above. But still, the booker
function is still able to access the passengerCount
variable of the secureBooking
function that should no longer exist.
You've certainly already guessed that what makes this possible is a closure. But before I explain how the closure works, I want you to appriciate once more, how starnge and beautiful this is. So again, the booker
function is a function that exists in the global scope. And the environment (secureBooking
) in which the function was created, is no longer active. But still, the booker
function somehow continues to have access to the variables that were present at the time the function was created. And in particular, the passengerCount
variable. Hence, that's exactly what the closure does.
So we can say that a closure essentially makes a function remember all the variables that were present or existed at the time it was created (The function's birthplace). So we can imagine the secureBooking
function as being the birthplace of the booker
function. And the booker
function remembers all the variables that were present at the time it was created.
This cannot be explained only with the scope chain. We also need to understand closure. Hence let me now explain how it actually works. When the booker
function is executed, that is, after the secureBooking
function has finished executing, and has been removed from the stack, the first thing that's going to happen is that a new execution context is created and added to the stack.
The variable environment of the booker
exection context will be empty because the booker
function doesn't have any variable declarations. Now what about the scope chain? Well, since booker is in the global scope, it is simply a child scope in the global scope.
You've probably already seen the problem. How will the booker
function access the passengerCount
variable? It's no where to be found in the scope chain. Hence, this is where we start to unveil the secret of the closure. This is the secrete: Any function always has access to the variable environment of the of the execution context in which the function was created.
In the case of the booker
function, it was born in the execution context of secureBooking
which was popped off the stack previously. This therefore gives the booker
function access to the variable environment of secureBooking
. Thus, access to the passengerCount
variable. And this is how the booker
function will be able to read, and update the passengerCount
variable. Hence it is this connection that we call closure.
I'm going to say it over again to make sure you get it.
It might still sound confusing but don't worry. I'll provide some familiar analogies at the end of this lecture. For now we are just trying to the mechanism behind the closure. How it all works behind the scenes. So, what matters the most here is that the booker
function has access to the passengerCount
variable because it's basically defined in the scope in which the booker
function was created.
So in a sense, the scope chain is actually preserved through the closure, even when a scope has already been destroyed because its execution context is gone. THis means that even though the execution context has actually been destroyedthe variable environment somehow keeps living somewhere in the engine.
Hence, we can say that the booker
function closed-over its parent's scope or variable environment.
This includes all function arguments, even though in this example, we don't have any. The closed-over variable environment now stays with the function forever. It will carry it around and be able to use it forever. To make it more digedtible, we can also say thet thanks to the closure, a function does not lose connection to variables that were present at the time it was created.
Let's now see what happens when we execute the booker
function. It attempts to increase the passengerCount
variable. However, this variable is not in the current scope. Hence, JavaScript will immediately look inti the closure and see if it can find the variable there. And it does this even before looking at the scope chain. So if we had a global variable called passengerCount
set to 10, JavaScript would still first use the one in the closure.
This shows us that the closure has priority over the scope chain. Hence after executing the booker
function for the first time the passengerCount
variable becomes one and the corresponding message is logged. And then the execution context is popped off the stack.
Then execution moves on to the next line. We get a new execution context, and a closure is still there, still attached to the function, and the value is still one. And so now the booker
function is executed again. Increasing the passengerCount
variable to two, and logging a message again. And that's what closures are and how they work behind the scenes.
Alright, all this seems quite complet. So, let me now give you a couple of different definitions of closure. Some formal, and some more intuitive and easier to grasp.
Finally, we need to understand that we do NOT have to manually create closures. It is a JavaScript feature that happens automatically. We can't even access closed-over variables explicitly. And a closure is NOT a tangible JavaScript object.
To finish this lecture, let's have a look at the internal properties of a closure. To do so we use console.dir
on the booker
function.
console.dir(booker);
As you can see, the result is a function with a bunch of properties, such as the name
property that we have already seen. We also have the scope internal property ([[Scope]]
). This property is basically the variable environment of the booker
function. Inside that scope property, we can actually see the closure coming from secure booking. Hence, this is where we see the passengerCount
which currently stands at three. And so the closure property is the variable environment of secureBooking
.
As a side note, whenever you see double brackets ([[Scopes]]
), it means that the property is an internal property, which we cannot access from our code. In the next lecture, we are going to take a look at some more examples of closures, and also analyze how they work because it's really important to understand the concept of closures. It's a feature that's used all the time in JavaScript, even without us realizing that closures are happening behind the scenes.