const - JS | lectureA Closer Look At Functions


A Closer Look At Functions

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 () {
    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.

Initial Code

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.

Global Scope

Now, when the secureBooking function is actually executed, a new execution context is created and added to the execution stack.

Secure Booking Context

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.

Variable Environment

This variable environment is also the scope of the function. Hence, the scope chain of this execution context looks like this:

Scope Chain

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.

Return Statement

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.

Removed From Call Stack

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 Result

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.

Booker Scope Chain

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 bookerfunction will be able to read, and update the passengerCount variable. Hence it is this connection that we call closure.

The Closure

I'm going to say it over again to make sure you get it.

  1. A function has access to the variable environment of the execution context in which it was created. Even after that execution context has been removed from the stack.
  2. Closure: Variable Environment attached to the function, exactly ast it was at the time and place the function was created.

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.

Parent Scope Closed

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.

Booker First Execution

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.

  1. A closure is the closed-over variable environment of the execution context in which the function was created, even after that execution context is gone.
  2. A closure gives a function access to all the variables of its parent function, even after that paarent function has returned. The function keeps a reference to its outer scope, which preserves the scope chain throughout time.
  3. A closure makes sure that a function doesn't loose connection to variables that existed at the functions's birth place.
  4. A closure is like a backpack that a function carries around wherever it goes. This backpack has all the variables that were present in the environment where the function was created.

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.


Closure Internal Props

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.