Most developers get confused with or fail to understand the concept of Closures in JavaScript because of its complexity. Many developers point to Closures as one of the most challenging concepts in JavaScript to fully understand and implement. Nevertheless, let me simplify Closures for you to understand it thoroughly in this article, and then you can explain it to others in your development space.
To understand the concept of Closures, I suggest you first grasp a solid understanding of the Call Stack, Execution Contexts, Variable Environments, Scope Chains, and Lexical Scoping.
What are closures?
By definition, Closures are a magical part of the browser's JavaScript engine that remembers the variable environment of the Execution Context in which its connected function was born. It can easily recall the variables of the surrounding environment within which it got created. For the same reason, some people define Closures as a function since it enables a function to access those local variables of a surrounding environment.
You can imagine Closures as the smart kid who remembers concepts from the sixth grade even after leaving them behind years ago. Let us break down the definition of this awkward concept and learn it efficiently. Take an example of a function that returns another function as the outcome of invoking it. Additionally, store the returned function in a variable to call it later.
let numOfReaders = 5;
const newReaderCount = () => {
let numOfReaders = 0;
return function () {
numOfReaders++;
console.log(numOfReaders);
};
};
const readers = newReaderCount();
readers();
We defined an arrow function called newReaderCount(), which holds a variable numOfReaders. When we invoke the newReaderCount() function, it returns another function, which increments the value of numOfReaders and logs it to the console. After calling the newReaderCount() function, we store the new returned function in a variable called readers. Remember, functions in JavaScript get treated as First-Class citizens. Thus, we can save them in a variable and invoke them later. Also, ignore the first line of the code snippet for now.
What is the problem with this example, though? How do Closures help?
The problem arises when we try to invoke the readers() function. If you notice, we stored the new function in the reader variable outside the scope of newReaderCount(). The readers() function needs access to the local variable of newReaderCount(). The variables within the newReaderCount() function are limited to their local scope. We cannot access them outside of those curly braces.
Therefore, JavaScript will automatically take the help of the Closure attached to the readers() function and access the variables of the Execution Context in which readers() got created. We gave the readers variable a value of a function, but in reality, the function got created inside newReaderCount() itself when we invoked it during the initialization of readers(). For the same reason, the Closure of readers() will refer to the variable environment of the Execution Context of newReaderCount() and simply access the current value of the variable.
Hold on, how is that possible?
You may say - "Wait, I learned that the Execution Context of a function leaves the Call Stack after its execution. So, how can Closures access the Execution Contexts of functions that are gone?".
The Execution Context of the newReaderCount() function left the Call Stack with its variables when we invoked it to store its output in the readers variable. The variables were only present when the readers() function received the function as a value, but those variables were not present when the readers() function received a function call. Additionally, the readers() function is inside the Global Scope instead of inside the newReaderCount() function, and we cannot access or modify values outside of the scope of newReaderCount(). So, how could the readers() function alter the numOfReaders variable?
Here's where the magic of Closures appears. The variables were inaccessible to us as the newReaderCount() left the Call Stack, but Closures enabled us to access them irrespective of where newReaderCount() currently resided in the execution process. The readers() function retrieved the numOfReaders value because functions like these always have access to the variable environment in which they get instantiated. In our case, the Closure of the readers() function remembered the variables in the Variable Environment of the native Execution Context of newReaderCount().
The connection between the readers() function in the Global Scope and the variable environment of the newReaderCount() function is known as a Closure. The Closure of readers() represents the attached variable environment of the Execution Context of the newReaderCount(). The scope chain of the newReaderCount() function gets stored within the Closure of readers() function even when the scope of the newReaderCount() gets destroyed due to the loss of the Execution Context. The variable environment keeps living somewhere in the JavaScript Engine.
You could relate to this scenario when your parents informed you which relatives and people were present when you were born. After that point, you suddenly never forget the names of those people, and neither does a Closure. That is the entire point of Closures. They merely remember the variable environment. You can imagine the newReaderCount() function as the birthplace of the readers() function, and the Closure retains all the variables present at the birthplace of the readers() function.
Without the help of Closures, the returned function stored in the readers variable wouldn't be able to access the numOfReaders variable. A closure gets attached to each function that references a variable environment outside its reach.
Someone told me Closures are functions
The developers who define closures as a function indirectly refer to the readers() sort of functions as Closures. I tend to hold a different view and avoid defining Closures as a function that can access the variable environment.
The Global Context gets Ignored
The JS Engine will look for the variables required by the readers() within the Closure and attempt to find them. Before checking whether the variables exist in the Scope Chain, it will first check the Closure. In other words, JS will try to find the variables in the Closures first instead of looking inside the same local scope of the readers() function or the Global Scope.
For example, take the variable numOfReaders on the 1st line that I asked you to ignore. If we have a variable in the Global scope with the same name, JS will still first try to find the variable in the Closure and ignore the same variable present in the Global Scope. If JS cannot find it in the Closure, it will look into the Local and Global Scope. JS will ignore the numOfReaders in the Global Context. Instead of looking for it there, it will move to the Closures, find the variable in the Closure, increment it, and eventually log 1 to the console since the previous value of numOfReaders was 0 inside the variable environment of newReaderCount().
Even if we call the readers() function multiple times, it will use the last assigned value as Closures always remain attached to the function. When the readers() leaves the Call Stack, the respective Closure acts as the best mate and never disappears. The Closure and readers() are in an aromantic relationship now.
According to Fireship, the interpreter of JavaScript creates a Closure and stores each variable with its value in the Heap Memory as this type of memory gets enhanced by the Garbage Collector of JavaScript, unlike the Call Stack, which represents a temporary memory only for execution purposes.
Accessing & Modifying Closures
We cannot access or modify the values of Closures. Closures are not an explicit feature in JavaScript. We do not need to create them manually, and JS automatically creates them in similar situations. We can only determine and observe those situations and use them to our advantage. We cannot directly access the variables, but we can see the values of a function's Closure ambiguously.
We can use the console.dir() method to view the internal Closure property. Take a look at the example below.
console.dir(readers);
Since we invoked the readers() function, the value of numOfReaders got incremented, and thus the latest value of it reflects as 1 when we use the console.dir() method. If you notice, the Closure is nested within [[Scopes]]. In short, the double brackets represent internal properties that cannot get accessed directly through our code. Hence, we cannot modify or access Closure properties.
By now, I hope you have understood the concept of Closures. If you haven't, take a look at the examples below.
The Multiplier Example
I extracted an example from Marjin Haverbake's book Eloquent JavaScript to illustrate the idea of Closures in a proper code solution. Before moving forward, remember that even the parameters or arguments of a function act as local variables. Thus, even parameters will be accessible to the Closures of nested functions.
function multiplier(factor) {
return function(number) {
console.log(number * factor);
}
}
const twice = multiplier(2);
twice(5); // 10
In the above example, we created a standard Pure Function called the multiplier(), which takes an argument named factor. We returned another function from multiplier() that takes one parameter named number and multiplies it with the factor provided by the outer scope.
Till we invoke the twice() function, the Execution Context of multiplier() already leaves the Call Stack, but it still allows access to its local variable called factor. The twice() function creates a Closure that holds the value of the factor variable present in the variable environment of the Execution Context of the multiplier() function.
Therefore, once the twice() function gets executed, it obtains the value of factor from the Closure attached to itself and logs 10 to the console. Let us also look at an infamous interview question that interviewers ask in most technical interviews.
Tricky Closures
Many interviewers ask about the output of the following code and how it would all work in technical interviews revolving around JavaScript or Front-End development. You must know the concept of Closures to answer the question flawlessly. Also, a quick disclaimer - I discourage using VAR in any case. You must use LET and CONST to avoid unnecessary problems in your program. The following code snippet includes VAR because the original interview question has that for a reason you will discover soon.
Developers might say the following snippet will return 0, 1, and 2, but you would be wrong.
for (var i = 0; i < 3; i++) {
setTimeout(function log() {
console.log(i);
}, 1000);
}
1st Answer (Non-Closure)
We have a for-loop that repeats the code inside it three times before it terminates itself. As the for-loop keeps iterating through the code, it creates three different setTimeout() timers for 1000 MS. It continues three iterations over the code inside the loop. Eventually, the timers begin to trigger one by one. Since the last value of "i" was "3" after the third iteration, each timer uses the latest local value and the number "3" gets printed thrice in the console. The loop iterates thrice through the setTimeout() function quickly, but the three log() functions only begin to execute themselves after a second, and they all end up printing "3" to the console.
2nd Answer (Considering Closures)
The first answer addresses why the loop prints three to the console multiple times. However, it doesn't answer what if we want to print 0, 1, and 2 to the console instead of 3 like intended earlier.
To print the correct values to the console, you must replace the "var" with the "let" keyword in the initialization block of the for-loop. The variables using VAR get hoisted to the parent scope, preferably the Global Scope, and thus when you replace the declaration keywords, you get the result as 0, 1, and 2.
The interpreter of JavaScript shifts the declaration of the "i" counter to the Global Scope from the Local Scope of the for-loop. When we use the LET keyword, the interpreter keeps the "i" counter as a local variable. Therefore, no modifications happen. No wonder why people don't prefer VAR nowadays.
for (let i = 0; i < 3; i++) {
setTimeout(function log() {
console.log(i);
}, 1000);
}
The final solution would look like the above code snippet. When using VAR, we are mutating the value in the Global Scope instead of mutating a local variable restricted to the walls of the for-loop. Nonetheless, you might also wonder why and where we use Closures. Let's discuss that.
Why and Where do we use Closures?
Unknowingly, we use Closures in many scenarios paradoxically in the most basic solutions where a function tries to access the value of the Global Scope, as shown in the example below.
const nameOfAuthor = 'Afan Khan';
function accessGlobal() {
console.log(nameOfAuthor);
}
accessGlobal(); // Output: "Afan Khan"
Do you see the Closure playing its role here? We also use them while working with HTTP clients like Axios or fetching data using the fetch() API. Other than that, we also use closures in Currying, which is a part of Functional Programming.
Essentially, a Closure allows you to access the variable environment of the parent Execution Context irrespective of whether it exists. You can use it in situations that match the above statement or other examples I cited in this article.
Why is this concept difficult to understand?
I believe it is because developers have overcomplicated this concept. While researching to understand this concept, I came across many articles and videos by renowned creators that identified closures as functions. For a beginner, that definition could go over their head. Even I got confused (more like frustrated) when I heard everyone calling them functions. After researching for hours, I discovered that they are referencing functions like readers(), log(), and accessGlobal() as Closures. Instead of calling them as a part that is attached to those functions, developers are identifying those functions themselves as if they are Closures.
It makes the concept more difficult to comprehend. When beginners decide to learn this concept, they end up getting confused due to the different definitions of everyone. As of late June 2023, the Wikipedia page dedicated to Closures in computer programming says that Closures are a technique for implementing lexically scoped name bindings in a language with first-class functions.
Nevertheless, I wanted to clear up the confusion for developers. Instead, a Closure represents a variable environment we cannot access or modify. We can only observe that Closures are in specific programs or functions and sometimes use them to our advantage in different scenarios. I hope the explanation in this article helped you understand Closures.
Summary
A Closure represents the variable environment of an Execution Context in which a particular function was born. You can also imagine a Closure as a backpack that a function keeps with itself with a bunch of variables inside it. Whenever the function enters or leaves a room, it roams with the rucksack and never forgets it anywhere. Whenever JS cannot find a value, it will look into the backpack of a function to check if the variable exists there. Otherwise, it will try to locate them in the room (Local Scope) or the entire building (Global Scope).
By the way, I am writing a book called CrackJS, which is based on and covers the fundamentals of JavaScript. If you want to learn more about JS, follow me on this journey of writing this book with these articles trying to help developers worldwide.
If you want to contribute to this article, drop a comment with your opinion, and if I should change anything in this article. I am also available via E-mail at hello@afankhan.com.
You can find the references, Code Snippets, and the raw draft in the following Notion document - Closures explained by Afan K.
Top comments (0)