Demystifying Closures in Javascript
A step by step guide to understanding closures in Javascript
Hello there😇😇! It's awesome having you here. Today, we'd be breaking down the concept of closures in Javascript. It's a not-so-complicated concept, yet not many developers, even with years of experience, fully understand exactly what closures are and how to intentionally implement them.
So, grab a cup of coffee and let's embark on a journey into the world of closures🚀🚀
So, why Closures?
The concept of closures to me, is the most elegant feature in the whole of Javascript, as it enables one to create high-level functionalities. Examples:
- Functions being able to run once (the function does not run if called more than once)
- Memoization (a computer science concept in which functions can remember the results of their previous running, therefore returning the same result, instead of computing the values, if called with the same arguments). Memoization is used to enhance the performance of applications.
- Many design patterns, like Node's module pattern were built using the concept of closures.
What are Closures?
According to MDN, "A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time".
Not clear? Read on🚀🚀
To fully break down the concept of closures and understand its beauty, we'd have to take a look at how the Javascript engine runs our code.
The Javascript engine has three main components, namely:
- The Execution Thread: This is what runs through and executes our code. It runs through our code line by line and executes the code as it goes.
- The Variable Environment: This is where all the data available in a particular scope is stored. A new one is created every time a function is called.
- The Call Stack: Stacks are one of the many traditional ways of storing information in computer science. The call stack is used to monitor the current scope in which the Javascript engine is running its code. Whenever a function is called, it is added to the call stack and popped off when it finishes running.
Let us take a look at the way Javascript runs this code:
1. const name = 'Justin Bieber';
2.
3. function sayHi(name){
4. console.log('Hi ' + name );
5. }
6.
7. sayHi(name);
8. sayHi('Jeremiah');
9.
We can easily tell what this code would do. But let us look more deeply at how the engine would run this piece of code.
- The Execution Thread, as I said earlier starts from line 1, finds an instruction there to declare a constant
name
in the memory (the global memory in this case) and stores the valueJustin Bieber
in it. Here,name
serves as a label for the space in memory that has the valueJustin Bieber
. (A reference we'd call it🙂). - Next, the Execution Thread moves on to line 3 and finds a function declaration with the label
sayHi
. This stores the whole content of the function (the whole code inside it) in the global memory with the labelsayHi
. Note that at this point, the Execution Thread does not evaluate the function, it just stores the code inside it in memory. Moving on to line 7, the Execution Thread, finds a call to the function
sayHi
. We must understand what happens when we call the function. First, the engine checks the memory for the value of thesayHi
function. When it finds it, it does a couple things:- It adds the
sayHi
function to the call stack. - It then creates a new Execution Context (an execution context is like a space where code can be run). This execution context has its own (local) variable environment (memory) and execution thread.
NOTE: Before the code starts running in line 1, a global Execution Context was created (with its variable environment (global) and execution thread).
- It then declares a new variable in the
sayHi
variable environment with the labelname
. Note that thisname
is different from the one in the global memory. The value ofname
is the argument passed into thesayHi
function. - Its execution thread moves on to the next line, line 4, and finds a call to the
console.log
function with arguments'Hi ' + name
. At this point, the Javascript engine does not know the value ofname
, it then performs a quick lookup in the local variable environment and returns its value into the console.log function which logs'Hi Justin Bieber'
to the console. - Now, the
sayHi
function is done, it gets popped out of the call stack and its Execution Context is destroyed, alongside all the data that was declared in that context.
- It adds the
The Global Execution thread moves on to line 8, and finds a call to the function
sayHi
. The same process in Step 3 repeats again.sayHi
is added to the call stack once again.- A new Execution Context is created.
- The function is evaluated.
- It gets popped out of the call stack.
- Its Execution Context gets deleted.
Wow! That was quite a lot there! I hope you followed through and got the hang of it👌. If not, please try reading it again for a better understanding.
Just to recap, a new Execution Context is created every time a function is called and destroyed when it's done with its evaluation.
In lines 7 and 8, the sayHi
was called, and each time it was called, a new Execution Context was created and destroyed after evaluation.
Now let us look at another code snippet:
1. let name = 'Justin Bieber';
2.
3. function createBot(){
4. function greeter(person){
5. console.log('Hello ' + person);
6. }
7. return greeter;
8. }
9.
10. const greetingBot = createBot();
11. greetingBot(name); // logs 'Hello Justin Bieber'
12. greetingBot('Jeremiah'); // logs 'Hello Jeremiah'
Let us look closely once again, how the Javascript engine would execute this code.
- The global Execution Context is created, and the execution thread starts executing from line 1.
- From line 1, it finds a variable declaration with the label
name
and a value'Justin Bieber'
. It stores this variable in the global variable environment. - Moving on to line 3, it finds a function declaration with the label
createBot
. It takes the whole code inside this function and then stores it as well in the variable environment (global memory). - Moves on to line 10, it finds a constant declaration with the label
greetingBot
, which is initially uninitialized. For it to get the value to assign to thegreetingBot
constant, it evaluates (calls) thecreateBot
function. - As usual, when a function is called, the function is added to the call stack, a new Execution Context is created.
- Inside the
createBot
Execution Context now, the execution thread is in line 4, it finds a function declaration with the labelgreeter
. It then takes the whole code inside thegreeter
function and stores it in its (createBot
's) local variable environment. Note: This function is not being called yet. - Still inside the
createBot
Execution Context, the execution thread is in line 7, it finds areturn
statement. It thereforereturns
thegreeter
function to thegreetingBot
constant. And now, thecreateBot
function has finished its evaluation, it is now popped off the call stack and its execution context destroyed, alongside the variable environment, with all the data therein (that includes thegreeter
function). - The
createBot
function has been evaluated and the value assigned to the constantgreetingBot
. In line 11, we are calling thegreetingBot
function withname
as a parameter. Now, this might seem confusing since thegreetingBot
function was created inside thecreateBot
function which has been evaluated and its variable environment destroyed. - So, the
greetingBot
function is the samegreeter
function that was declared inside thecreateBot
execution context. It just has a different name (identifier or label) now which is accessible in the global execution context. (This isn't closure yet) - In line 11, the
greetingBot
function is called, it's added to the call stack, a new execution context is created. The local execution thread is now at line 4. - At line 4, there is an instruction to declare a variable with the label
name
and it is initialized with the function argument. - Line 5, the execution thread finds a call to the
console.log
function, which first fetches the value ofname
which was initially declared in line 4 and sends the value to theconsole.log
function in line 5, which goes on ahead to log'Hello Justin Bieber '
to the console. - The
greetingBot
function has now been evaluated, its execution context is destroyed and it's popped off the call stack. We're now in the global execution context. - In line 12, the
greetingBot
function is called with a different argument'Jeremiah'
, the whole process is repeated and'Hello Jeremiah'
finds its way to the console.
Yeah! So, in the code snippet above, we had a function createBot
which returns another function greeter
which was created in createBot
's execution context but is now being evaluated in the global execution context.
Now, take a minute. Let it all sink in
Great, I hope it's all sunk in.
Now, let's look at a very simple example of closures.
1. function counter(initialValue){
2. let count = initialValue || 0;
3. function increment(value){
4. count+=value;
5. console.log("Counter is at", count);
6. }
7. return increment;
8.
9. const incrementBy = counter(50);
10. incrementBy(25); // logs 75
11. incrementBy(10); // logs 85
Looking closely once again at how the Javascript engine would run this piece of code (try to do this on your own before you continue reading😎)
- The Global Execution Context is created so we can run our code. In line 1, we find a function declaration with the label
counter
. The whole code inside the function is copied and stored in the global variable environment. - In line 9, we find a constant declaration with the label
incrementBy
. For us to get the value ofincrementBy
, we have to evaluate thecounter
function with argument50
. This evaluation leads us to do a couple of things that I believe we already know. - The
counter
function is called (begins evaluation), it's pushed into the Call Stack, a new Execution Context is created with a new Variable Environment. We're back to line 1, but doing something different from earlier. - In line 1, we initialize a variable
initialValue
and give it the value passed in as an argument to the function (we initialize it to50
in this case). - In line 2, we have another variable declaration with the label
count
which is initialized toinitialValue
or0
(ifinitialValue
is falsey (undefined)) - In line 3, we find a function declaration with the label
increment
. We go on ahead to copy all the code inside the function and store it in the local variable environment. - In line 7, we return the
increment
function. It is now available in the global variable environment but with a different labelincrementBy
(it is the result of the evaluation of thecounter
function in line 9, which is now popped out of the Call Stack with its variable environment destroyed). - In line 10, we find a call to evaluate the
incrementBy
function. Remember that this function was created in thecounter
execution context and was made available in the global context via a return statement. Calling this function takes us to line 3, but note that thecounter
function is not being called again. It's already done with its evaluation. - In line 3, we have a variable declaration with the label
value
, which is initialized to the argument passed into theincrementBy
function,(25 in this case). - Moving on to line 4, we find an assignment to a variable
count
which was not initialized in theincrementBy
function scope. This seems to be an issue because when the counter function finished its evaluation, its variable environment was destroyed, alongside all the variables in it. So what happens, according to what we've seen so far,count
should beundefined
and the code snippet above is expected to fail.
Turns out that's not the case and the code happens to work perfectly.
There's something we're not talking about here: Whenever a function finishes its evaluation before its variable environment is destroyed and gets popped out of the call stack, there are actually two extra things the Javascript engine does:
- The Javascript engine looks through the code, searching for variables that were referenced in the outer scope by the functions returned.
- Having found the variables, it creates a special variable environment (a special backpack full of goodies), that contains all the variables required for the function to run without error.
In our example above, when we returned increment
in line 7, we got not just the function itself but an extra container of variables required to make increment
run without error. This is what closure is.
In this case, we got the variable count
and its value inside our special backpack. This implies that line 4 would run perfectly since count
is defined in our backpack. At this point we expect count
to be equal to 50
since that was its value when the counter
function was called. It's expected that the value of count
is equal to count + value
which should now be 75
(50 + 25).
Line 8 logs the value of count
to the console
.
Notice how these extra two steps taken by the Javascript engine gives us this very beautiful feature that helps us somehow retain the value of a function that has completed its evaluation.
In line 9, the whole process is repeated. Note now that count
is already 75
, so calling incrementBy
with an argument 10
would go on ahead to increment the value of count
in our backpack, making it 85
.
You'd agree with me that this is a very beautiful feature in Javascript.
We can go on ahead to create multiple backpacks by calling the counter
as many times as we want. Take a look at this piece of code:
1. function counter(initialValue){
2. let count = initialValue || 0;
3. function increment(value){
4. count+=value;
5. console.log("Counter is at", count);
6. }
7. return increment;
8. }
9. const incrementBy = counter(50)
10. incrementBy(25) // logs 75
11. incrementBy(10) // logs 85
12.
13. const incrementFromTen = counter(10)
14. incrementFromTen(20) // logs 20
15. incrementFromTen(50) // logs 70
Lines 13, 14 and 15 are just a repetition of the whole process, just that now we have a separate backpack for different functions.
Now, let's see if you understand MDN's definition of closures.
Sentence 1: "A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment)". This is simply because we have a function returned with a special backpack of variables (references) to its surrounding state (variable environment).
Sentence 2: I think it's clear enough Sentence 3: "In JavaScript, closures are created every time a function is created, at function creation time". If you've been following closely, you'd notice that most of the examples we look at had functions being returned from a function. This is the way closure is created.
Yippee🥳🥳
SUMMARY
Now, we understand how closures work, let's summarize everything we've looked at today.
- First, the Javascript engine is made up of three parts: the variable environment, call stack and the execution context.
- Closures are created when a function retains references to variables in its outer scope.
- Node's module pattern was built using the concept of closures.
RESOURCES
Here are some resources to really understand the concept.
- Javascript, The Hard Parts on Frontend Masters
- Understanding and Applying Closures in Javascript, Medium
- MDN Article on Closures
- A CodeSandBox folder with some Real-life Usage of Closures
- A CSBin with challenges to practice your knowledge of closures
I hope you enjoyed reading this article.
Follow me on Twitter @jeremiahlena13