Closures in Javascript in Depth

The closure is one of the most important and elegant concepts of JS. It forms the most fundamental concept of many important functions and concepts in JS. It is a powerful concept that allows us to create many useful and powerful pro-level functions like once and memoize.

Many JavaScript design patterns including the module pattern use closure. Moreover, Closure allows us to build iterators, handle the partial application, and maintain state in an asynchronous world.

Before understanding and getting in-depth into the closure please keep in mind that whenever a function runs or invoked then it gets a new execution context with local memory. When a function gets called then the function is pushed onto the call stack. When the function starts executing we create a live store of data that can be called local memory, variable environment, or state. When the function finishes executing, the result is returned and execution context with its local memory is deleted and the function is popped off the call stack.

But with closures, we can create functions with memories. The closure is just a function that returns another function. The following is an example of closures.

function createFunction() {
 function multiplyBy2 (num){
    return num*2;
 }
 return multiplyBy2;
}
const generatedFunc = createFunction();
const result = generatedFunc(3);

Now, look at the following function without actually running it in the console. Go through the function theoretically and try to predict the result keeping in mind our current understanding and discussion so far.

function outer () {
  let counter = 0;
  function incrementCounter () { 
    counter++; 
  }
  return incrementCounter;
}
const myNewFunction = outer();
myNewFunction();
myNewFunction();

The execution will take as follows:

  1. First of all we are declaring a function named outer.
  2. Now we are declaring a const myNewFunction which is uninitialized.
  3. Then we are calling the outer function and it is put on the call stack. Now it will create a new execution context with the local variable environment.
  4. Inside the outer function, first, we declare a variable counter to 0. And we are defining a function incrementCounter and lastly returning the function incrementCounter. Now the outer is popped off the calls stack and its execution context gets deleted.
  5. Now the const myNewFunction gets initialized with the returned value of the outer i.e function definition of incrementCounter. Now the myNewFunction will be like. function myNewFunction (){ counter ++; }
  6. Now we are calling myNewFunction. It will be added to the call stack.
  7. This will create a brand new execution context and Local environment variable.
  8. In the function definition, we can see the counter is incremented but the counter variable is not declared or initialized.

Now theoretically this should result in an error the counter is not declared or initialized.

Now try to run it in the console, you will notice that it will not result in an error but something strange happens. The result will be:- 1 and 2

Lexical scope: One intricacy of JavaScript is how it looks for variables. If it can’t find a variable in its local execution context, it will look for it in its calling context. And if it doesn't found there, it will keep looking, until it reaches the global execution context. And if it does not find it there, it’s undefined.

But how this can be possible ?

The Answer is..... THE BACKPACK.

The Back Pack

The concept of the backpack was introduced to me by Will Sentance while I was going through Javascript the hard parts course. No doubt one of the best online courses taught by certainly the best instructor I ever had with excellent teaching skills.

Now let us go back to the previous code and at the instance when the incrementCounter is returned to the const myNewFunction (Line Number: 5).  Whats happens at that stage is that myNewFunction somehow maintains the bond to outer function local memory i.e, the local memory of the outer function gets returned out and attached on the back of incrementCounter’s function definition. So outer’s local memory is now stored and attached to myNewFunction - even though the outer’s execution context is long gone.  When we run myNewFunction globally, it will first look in its own local memory (as we’d expect), but then in myNewFunction’s backpack.

What makes them so special that the function return will have a special hidden [[scope]] property attached with it which can be called a backpack for better understanding. That contains a reference to all the variables in the local memory of the main function that returned that function. And this way we can have a function that has access to the local memory of the main function even after it has been executed i.e even after its execution context is deleted. The closure gives our functions persistent memory.

The different name of the backpack

  1. Closed over ‘Variable Environment’ (C.O.V.E.)
  2. Persistent Lexical Scope Referenced Data (P.L.S.R.D.)
  3. Backpack
  4. Closure - Official
function outer () {
 let counter = 0;
 function incrementCounter () {
   counter ++;
 }
 return incrementCounter;
}

const myNewFunction = outer();
myNewFunction(); // 1
myNewFunction(); // 2

const anotherFunction = outer();
anotherFunction(); // 1
anotherFunction(); // 2

We call the outer() function and store the returned value with the backpack in const myNewFunction. The myNewFunction will have access to all the [[scope]] property containing the live data of outer in the backpack or closure. Now the myNewFunction will have a definition of incrementCounter and will be something like:

function myNewFunction () {
  counter ++;
 }

As soon as we call myNewFunction for the first time, a brand new execution context is created and put on top of the call stack. Now it will look for the counter variable in its local environment, and it will not find it there. And then it will look for it in the backpack and will find it there will the value of 0. And then increment it and return it and the function is popped off the call stack.  

Now we are again calling myNewFunction and now again brand new execution context is created and put on top of the call stack. Now again it will find the counter in the backpack and not in the local memory.

But this time the value of the counter is 1 as set by the previous myNewFunction call as the backpack is still the same as we are instance of outer() is the same. It will increment it to 2 and return it and the function is popped off the call stack.

If we run outer again and store the returned incrementCounter function definition in anotherFunction, this new incrementCounter function was created in a new execution context and therefore has a brand new independent backpack.

Every instance of the outer function being executed create a brand new backpack or closure.

Uses of Closures

  • Gives our functions persistent memory.
  • Helper functions: Everyday professional helper functions like ‘once’ and ‘memoize’
  • Iterators and generators: Which use lexical scoping and closure to achieve the most contemporary patterns for handling data in JavaScript
  • Module pattern: Preserve state for the life of an application without polluting the global namespace
  • Asynchronous JavaScript: Callbacks and Promises rely on the closure to persist state in an asynchronous environment

That's all! If you have any suggestions or find any errors in the article please let me know in the comment section. If you enjoyed the article please share it on social media platforms.