Unearthing JavaScript: Closures Demystified

Kartik Srivastav
6 min readDec 24, 2023

--

Closures are one of the most beautiful phenomena of JavaScript. Numerous behaviours of your JavaScript code are explainable with the help of closures. So, let’s dive deep into closures and understand their importance.

Getting Started

Before defining closure, let’s understand a few terminologies:

  • Scope of a function: It refers to the region of code where a variable defined in the function can be accessed.
  • Lexical Environment: A function’s lexical environment is a context where it’s located physically in the code, along with the reference to the outer environment in which the function was declared.
  • Scope Chain: A function has its own scope and also a reference to the outer environment, and through this reference, it can access the info about the scope of its outer environment. The chain of scopes thus formed with multiple references is called the Scope Chain.
  • Execution Context: An execution context is a wrapper in which the code is executed and managed. It sets up the memory of variables and references to outer environments and executes the code. To learn about how execution contexts work, you can refer to this article.

To briefly define closures, a closure is a combination of a function along with reference to its lexical environment. When a function is defined in another function, we can access the outer function's variables even after its execution is completed with the help of closures through the reference of outer environments. Let’s take a basic example for understanding a closure:

function outer()
{
var num = 30;
function inner()
{
console.log(num);
}
inner();
}

outer();

Yes, that’s it! The above snippet is an example of closure. Let’s analyze this code’s execution:
When the outer function is invoked, an execution context is created for it in the call stack. The memory for num and inner function is set up in the creation phase of the execution context. When the inner function is invoked, it refers to the value num from the parent function’s scope and logs the value. And that’s exactly how we defined closures. Here, the inner function, along with reference to its parent’s lexical environment, forms a closure.

You can also check this in your own browser console. Just put a debugger in the line where the value is logged from the Sources tab in the dev tools and check under the Scope section; you will see the closure.

Closure in dev tools

Digging Deeper

While this behaviour may seem usual, the concept of closures brings a stark difference in how JavaScript executes the code. To demonstrate this, let’s change the above code a little bit, and instead of calling the inner function inside the outer function, let’s return the inner function.

function outer()
{
var num = 30;
function inner()
{
console.log(num);
}
return inner;
}

var x = outer();
x();

Now, let’s understand what the execution will look like. When we call the outer function in the second last line, its execution context will be created, and its code will execute. Then, after the execution is complete and the inner function is returned, the execution context of the outer function will pop from the call stack and will not exist anymore.

Then, in the next line, when the variable x, which stores the inner function, is called, it will try to find the value of num to print. But here’s the interesting part: num was set up in the execution context of the outer function, and it shouldn’t exist anymore because the execution context has already been popped off from the call stack. So, what will be the output? Will it be 30, undefined, an error or anything else? Amazingly! the output will be 30 because of the closure, the inner function formed with the outer function's scope. In the variable x, not just the inner function but its closure with the outer function was returned. The closure remembers the reference to the outer environment and hence remembers the value of num. So even after the outer function no longer exists, its variables are still accessible through the closure, and that’s the beauty of it.

Understanding weird phenomena

To use closures properly, we need to understand some weird situations that come with them. Let’s consider the following code where we want to get an array of functions to log some values:

function outer()
{
var funcArray = [];
for(var i = 0; i < 3; i++){
funcArray.push(
function(){
console.log(i);
}
)
}
return funcArray;
}

var functions = outer();
functions[0]();
functions[1]();
functions[2]();

This may seem like a simple code to log the value of the iterator. Why don’t you give it a go and try to figure out the output? You might think it is “0 1 2” as the logic looks pretty straightforward, but the actual output is “3 3 3”.

Let’s look carefully and understand why this happened. The array we returned contains functions which print a variable’s value, and that variable is in the scope of the parent of those functions. So naturally, closures will come into the picture. And here is a very important thing to note about closures: when a closure is formed, the function remembers the Reference to the variables in the outer lexical environment, not the Value of those variables.

In the above example, the value of i is 3 when the execution of the loop is complete. Another thing to note is that we declared i using the var keyword, meaning the variable was function-scoped, so each function pushed into the array shared the same reference to it. Hence, when we execute these functions, they refer to the same variable i with the help of closures and log its value, which is 3.

But what if we want to achieve what we actually expected, i.e. the output “0 1 2”? Well, there are many ways to do so; we can use IIFEs(Immediately Invoked Function Expressions), but we would need to understand what those are, and that’s a story for another time. The simplest way is to use let instead of var while declaring i in the for loop. Why? Because let is block-scoped. Basically, in each iteration of the loop, there is a fresh copy of i, and reference to that copy is bound to the function being pushed into the array. And that function forms closure with the new value of i in each iteration. As a result, the output will be “0 1 2”. Another ironic way to solve this is to use an additional closure:

function outer()
{
var funcArray = [];
for(var i = 0; i < 3; i++){
function close(j){
funcArray.push(
function(){
console.log(j);
}
)
}
close(i);
}
return funcArray;
}

var functions = outer();
functions[0]();
functions[1]();
functions[2]();

The main reason for the same output value was that we weren’t able to get different values of i to be bound to the functions in the array. To solve this, we enclosed the push operation in another function named close with a parameter j, which will be logged into the console. In each iteration, we call the close function and pass the value of i as an argument. So now, the function passed into the array will form closure with reference to the variable j, which will have a new value for each iteration as we pass a new value of i in the close function in each call.

Conclusion

Closures are the core of multiple JavaScript concepts and thus are of utmost importance. For example, closures are used in Encapsulation, Currying, setTimeout, Iterators, Module Design Patterns, etc. And as with everything, closures have their cons. One of the most significant disadvantages of closures is increased memory usage. The reference to the variable of the outer environment is stored, and as it can be used anytime in the code, it is not freed by garbage collectors. Although there are smart garbage collectors in modern browsers to clear out unnecessary memory references, closures still contribute heavily to memory usage.

Thank you for reading this article. Understanding closures will help you explore much more beautiful concepts of JavaScript and will help you grow immensely as a JS developer. I hope you learned something new and exciting today. Feel free to share your thoughts and feedback in the comments. Happy Reading :)

--

--