DEV Community

Yash
Yash

Posted on

Disclosure of JavaScript Closures.

JavaScript closures are tough to wrap your head around the first time you encounter them. Some developers might come off forming a wrong mental model about closures as it is very easy to get closures in the wrong way.

Perhaps reading the code that uses closure in a linear fashion can be an easily misleading way to form a wrong mental model about it. In this post I am going to disclose what closures actually are.

Let's start by understanding how the JavaScript engine parses our code.

How JavaScript Engine Works

  1. It goes by the code line by line.

  2. Any function declaration and variables it finds is put in the global memory.
    (Putting these functions and variable in the global memory is called hoisting.)

   // Values like below are put in the global memory.
   const someVariable = 123

   function myFirstFunc() {
       console.log('This is my awesome function')
   }

   const mySecondFunc = function mySecondFunc() {
       console.log('This is my awesome function')
   }
Enter fullscreen mode Exit fullscreen mode
  1. At this point the JavaScript code is compiled, and the engine will again go line by line.

  2. When the engine hits a function it checks its global memory for the function and creates a temporary environment for that function which is known as its execution context.
    The fact that the function is pulled out from the global memory is worth emphasizing which you'll learn soon why.

The execution context has 2 parts - a memory and a place to execute the statements inside the function. This execution context is unique to the function.
The function is also added at the top of call stack, the global() always rests at the bottom of this call stack. The call stack basically tells the engine what to work on, so, the function on the top of JavaScript is what the engine will work on.

  1. All the arguments passed in the function are evaluated (if you pass in a variable a as an argument which was assigned a value of 1, then a is changed to 1),

  2. These evaluated arguments are added to the memory part of the execution context of the function. In the memory these arguments are saved by the labels given according to the parameters of the function.

   function myElegantFunction(myParameterOne, myParameterTwo) {
       console.log(myParameterOne, myParameterTwo)
   }

   myVariableOne = 'Hello'
   myVariableTwo = 'World'

   myElegantFunction(myVariableOne, myVariableTwo)

   /** myElegantFunction(myVariableOne, myVariableTwo)
    is changed to 
    myElegantFunction('hello', 'world')

    Let's see the memory part of the execution context of myElegantFunction,
    ----------
    myParameterOne: 'Hello'
    myParameterTwo: 'World'
    ----------
    As you can see how these arguments are saved according to the name of the parameter which we            referenced in the function declaration.
   **/
Enter fullscreen mode Exit fullscreen mode
  1. Now the statements inside of the function are executed one by one, if it contains any variable it is first looked in the memory part of the execution context of that function if the variable is not found then the engine tried to search for it in the global scope.

  2. The function is removed off the call stack and the global() proceeds to run the JavaScript code.

To give more clarity I have made a small video animation visually explaining this process exclusively for this post.

By now you must have understood how the call stack, execution context, and memory work all together to achieve the task of running your code. Keeping the above procedures in mind, this is the perfect time to introduce you to closures.

Getting close to closures

Let's consider a function -

function counterFunction() {
  let counter = 0;

  function increaseCounter() {
    counter++;
    console.log(counter);
  }

  return increaseCounter;
}
Enter fullscreen mode Exit fullscreen mode

The function counter is a higher-order function as it returns another function namely increaseCounter.
Let's declare assign this function to a variable.

const count = counterFunction();
Enter fullscreen mode Exit fullscreen mode

When JavaScript is executing the above line, it puts the function increaseCounter in its global memory. So what goes in the global memory with the label count is -

count: function increaseCounter() {
    counter++;
    console.log(counter);
  }

// NOTE: Use of ':' (colon) is representational.   
Enter fullscreen mode Exit fullscreen mode

Here's where things start getting interesting when we call count

count(); // Output is 1
count(); // Output is 2
count(); // Output is 3
Enter fullscreen mode Exit fullscreen mode

JavaScript is in fact, getting the function from the global memory,

function increaseCounter() {
    counter++;
    console.log(counter);
  }
Enter fullscreen mode Exit fullscreen mode

Here's another animated video for the execution of the above code.

As the execution context starts executing the statement, it encounters the variable counter, the first place it checks is the memory of the execution context itself and the next thing it should check is the global memory.

Anyone familiar with the working of the JavaScript engine should think it is impossible to get variable counter.

This is where closures come into the play. Let's go back to where we stored counterFunction().

const count = counterFunction();
Enter fullscreen mode Exit fullscreen mode

When the increaseCounter is stored in the count variable. The count variable literally carries with it the variables from the function counterFunction, which is the function increaseCounter was *return*ed from.

In this state it is said that - increaseCounter has a closure over counterFunction.

The value of counter is coming from the closure which increaseCounter carried. Every time we call counter++ we don't touch the counter in the counterFunction we update the counter variable in the closure of increaseCounter.

To demonstrate the fact that counter being updated is not the part of counterFunction() here's a neat trick.

const count = counterFunction()
const countTwo = counterFunction()

count() // Output is 1
count() // Output is 2
count() // Output is 3

countTwo() // Output is 1
count() // Output is 4
countTwo() // Output is 2
Enter fullscreen mode Exit fullscreen mode

If counter was being updated from the counterFunction() instead of the closures of the function count and countTwo then the output of countTwo() must have added on to the value updated previously by the count() function. But it does not happens.

Conclusion

I mentioned earlier how easy is it to develop a wrong mental model about closures, it is because we tend to read code linearly and tend to confuse lexical scoping to closures, they are similar but not the same.

Closures are a part of the scope of a function. You can be more clear about closures if you use the JavaScript debugger in your browser's developer tool to inspect where the variables are stored.

Chrome literally shows the closure to be a part of that function's scope which they are. Closures are not a link between two functions.

Top comments (2)

Collapse
 
symiel profile image
Samuel Kinuthia

Amazing article.
Good job, now I know the behind the scenes of JavaScript.

Collapse
 
yashguptaz profile image
Yash

Let me know what should I write on next :)