Demystifying Closures in Javascript

Demystifying Closures in Javascript

A step by step guide to understanding closures in Javascript

·

13 min read

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.

  1. 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 value Justin Bieber in it. Here, name serves as a label for the space in memory that has the value Justin Bieber. (A reference we'd call it🙂).
  2. 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 label sayHi. Note that at this point, the Execution Thread does not evaluate the function, it just stores the code inside it in memory.
  3. 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 the sayHi 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 label name. Note that this name is different from the one in the global memory. The value of name is the argument passed into the sayHi 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 of name, 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.
  4. 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.

  1. The global Execution Context is created, and the execution thread starts executing from line 1.
  2. 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.
  3. 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).
  4. 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 the greetingBot constant, it evaluates (calls) the createBot function.
  5. As usual, when a function is called, the function is added to the call stack, a new Execution Context is created.
  6. Inside the createBot Execution Context now, the execution thread is in line 4, it finds a function declaration with the label greeter. It then takes the whole code inside the greeter function and stores it in its (createBot's) local variable environment. Note: This function is not being called yet.
  7. Still inside the createBot Execution Context, the execution thread is in line 7, it finds a return statement. It therefore returns the greeter function to the greetingBot constant. And now, the createBot 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 the greeter function).
  8. The createBot function has been evaluated and the value assigned to the constant greetingBot. In line 11, we are calling the greetingBot function with name as a parameter. Now, this might seem confusing since the greetingBot function was created inside the createBot function which has been evaluated and its variable environment destroyed.
  9. So, the greetingBot function is the same greeter function that was declared inside the createBot 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)
  10. 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.
  11. At line 4, there is an instruction to declare a variable with the label name and it is initialized with the function argument.
  12. Line 5, the execution thread finds a call to the console.log function, which first fetches the value of name which was initially declared in line 4 and sends the value to the console.log function in line 5, which goes on ahead to log 'Hello Justin Bieber ' to the console.
  13. 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.
  14. 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 let that sink in.jpeg

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😎)

  1. 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.
  2. In line 9, we find a constant declaration with the label incrementBy. For us to get the value of incrementBy, we have to evaluate the counter function with argument 50. This evaluation leads us to do a couple of things that I believe we already know.
  3. 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.
  4. In line 1, we initialize a variable initialValue and give it the value passed in as an argument to the function (we initialize it to 50 in this case).
  5. In line 2, we have another variable declaration with the label count which is initialized to initialValue or 0 (if initialValue is falsey (undefined))
  6. 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.
  7. In line 7, we return the increment function. It is now available in the global variable environment but with a different label incrementBy (it is the result of the evaluation of the counter function in line 9, which is now popped out of the Call Stack with its variable environment destroyed).
  8. In line 10, we find a call to evaluate the incrementBy function. Remember that this function was created in the counter 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 the counter function is not being called again. It's already done with its evaluation.
  9. In line 3, we have a variable declaration with the label value, which is initialized to the argument passed into the incrementBy function,(25 in this case).
  10. Moving on to line 4, we find an assignment to a variable count which was not initialized in the incrementBy 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 be undefined 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:

  1. The Javascript engine looks through the code, searching for variables that were referenced in the outer scope by the functions returned.
  2. 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🥳🥳

let that sink in.jpeg

SUMMARY

Now, we understand how closures work, let's summarize everything we've looked at today.

  1. First, the Javascript engine is made up of three parts: the variable environment, call stack and the execution context.
  2. Closures are created when a function retains references to variables in its outer scope.
  3. Node's module pattern was built using the concept of closures.

RESOURCES

Here are some resources to really understand the concept.

  1. Javascript, The Hard Parts on Frontend Masters
  2. Understanding and Applying Closures in Javascript, Medium
  3. MDN Article on Closures
  4. A CodeSandBox folder with some Real-life Usage of Closures
  5. A CSBin with challenges to practice your knowledge of closures

I hope you enjoyed reading this article.

Follow me on Twitter @jeremiahlena13

Â