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:
- 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.
- 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?
- The local execution context of the function is removed from the call stack.
- 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
- 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)
So this looks pretty simple, but there is quite a lot going on here. Let's go through this line by line.
- 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. - 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 variablesquared
. 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. - 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 functionsquared
with the argumentx
passed into it. Beforesquared
is invoked, the variablex
is uninitialised. - The function
squared
is invoked. - A brand new local execution context is created – we shall call this "squared execution context".
- 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. - 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 expressiona * 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 parametera
, a new variablea
is declared in the local execution context and assigned to it is the argument that was passed in when the function was invoked which isx
. JavaScript will look in global memory forx
and find that the value is assigned to it is 5 and then assign the value 5 toa
. Note thata
is stored in the memory of the "squared execution context". - The value is returned out to global context and is stored in variable
y
. - 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.
- 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
}
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())
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)
Let's dive deeper into what exactly is going on in the above code.
- 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. - We head down to line 7 where we declare the variable
createdFunction
and assign to it the result of the invocation offirstFunction
. UntilfirstFunction
is invoked,createdFunction
is uninitialised. - 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. - 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. - In line 5, we return out the value of
secondFunction
to the global execution context where it is stored inside the variablecreatedFunction
. - 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.
- 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 ofcreatedFunction(10)
. - We invoke
createdFunction
with the value 10 passed in as an argument. - Upon invocation, a brand new local execution context is created, we will call this second local execution context.
- Second local execution context is added onto the top of the call stack.
- 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 variablex
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 variablex
with the value 10. - 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 thesecondFunction
is nested inside offirstFunction
but that is not actually the case. Remember that at this point first local execution context does not exist andcreatedFunction
is not being called inside offirstFunction
. 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 variablefinalResult
. - 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.
- 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()
Let's dive into this function line by line.
- In line 1 we declare the variable
doSomething
and assign to it a function definition inside of the global execution context. - We jump down to line 9 and declare the variable
doSomethingElse
and assign to it the result of the invocation ofdoSomething
. UntildoSomething
is invoked,doSomethingElse
is uninitialised. -
doSomething
is invoked and we create a brand new local execution context which we will refer to as doSomething execution context. - doSomething execution context is added to the top of the call stack.
- We jump to line 2 inside the body of
doSomething
. - We declare the variable
num
and assign to it the value 1. This variable is stored inside the memory of doSomething execution context. - 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. WhendoubleNum
is defined, what also happens is that JavaScript creates a bind to the local variable environment through a hidden scope property on the functiondoubleNum
. This means that when we return outdoubleNum
, attached to it is all of the local data. All the variables in scope at the timedoubleNum
is defined are attached todoubleNum
.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 ofdoubleNum
are all of the variables inside of doSomething execution context which is basically justnum
. - 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 todoubleNum
and its closure is returned out to the global context and stored in the variabledoSomethingElse
. - 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
andnum
are erased as the doSomething execution context no longer exists. - We are now at line 10 and we invoke
doSomethingElse
. - A brand new execution context is created which we will call the doSomethingElse execution context.
- doSomethingElse execution context is added onto the call stack.
- 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 whendoSomethingElse
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 incrementnum
by 1. Before JavaScript performs the increment operation, it needs to get a value fornum
. So where will it look first? You guessed it – in the closure. Stored in the closure attached to the function definition that was assigned todoSomethingElse
, is the variablenum
which has a value of 1. JavaScript then incrementsnum
by 1. This value is updated within the closure ofdoSomethingElse
. This means that nownum
is 2. - We then console log out the value which is 2.
- Back at line 11, we invoke
doSomethingElse
again and repeat steps11
-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 incrementnum
, it looks inside the closure and finds that the value is 2 (as a result of the first invocation ofdoSomethingElse
) and the result of the operation will be 3. - 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 ofdoSomething
. - 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 variabledoSomethingBetter
along with the function definition. Remember this as it will prove important in the next couple of steps –doSomethingElse
anddoSomethingBetter
have different closures. - Now at line 13, we invoke the function
doSomethingBetter
. We repeat steps11
-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 ofnum
. You might be tempted to say that when JavaScript looks inside the closure for the value ofnum
it will be 3 because of the previous invocations ofdoSomethingElse
. This is not the case. BecausedoSomethingElse
anddoSomethingBetter
have different closures, the value ofnum
initially in the closure ofdoSomethingBetter
is 1 as this was the value ofnum
when the function definition was returned out todoSomethingBetter
. Having said that, the value ofnum
inside the closure ofdoSomethingBetter
will be incremented by 1 resulting in 3. - 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. - A brand new execution context is created we run through steps
11
-14
again taking into account the fact that the value ofnum
inside the closure ofdoSomethingBetter
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)