1. Introduction
In this tutorial, we’ll discuss one of the most important concept of JavaScript – Closure. Closure is a popular interview question. Closure is not a special syntax and is not related to a particular class or library. You can think it as a concept. Closure is created as a result of writing code that relies on lexical scope. You may have unintentionally created a Closure without recognizing it.
So, what actually is a Closure?
A Closure is when a function is able to access its lexical scope even when it is executing outside its lexical scope.
Let us see an example of Closure.
function outer() { let x = 5; function inner() { console.log(x); } return inner; } let outerFn = outer(); outerFn(); // 5
In this example, function inner()
has a closure over the scope of outer()
, because inner()
is nested inside of outer()
. Let us discuss this in detail.
The function inner()
is nested inside outer()
function so the function inner()
has lexical scope access to the inner scope of outer()
. The function outer()
returns the function object itself, inner
in this case. The statement let outerFn = outer();
executes the outer()
function and assigns it to a new reference variable outerFn
.
Once the outer()
function is executed, we expect whatever is in the inner scope of outer()
will be collected or destroyed by the garbage collector. But it does not happen in our example, as we are still able to execute inner()
function and the reference is kept alive for later use using outerFn
. So when outerFn()
is executed, inner()
is executed which is able to access the variable x
. So a Closure allows the inner()
to access its lexical scope even when it is executing outside its lexical scope.
2. Understanding Closure
A closure is a combination of the following two things:
- A Function
- A reference to the environment/scope in which that function is created
In other words, whenever we define a function in JavaScript, that function saves a reference to the environment in which it was created. This is what is referred to as a closure: a function along with a reference to the environment in which it is created.
Closures allow a nested function to access the declarations inside the containing function, even after the execution of the containing function has ended.
function outerFunction() { const outerVar = 99; return function inner() { console.log(outerVar); }; } const innerFunction = outerFunction(); innerFunction(); // 99
Here the innerFunction
, function returned by outerFunction
, have access to the outerVar
variable declared in outerFunction
even after the outerFunction
execution is complete.
The above code works because JavaScript functions always create closures when they are created. In some programming languages, a function’s locally defined variables only exist for the duration of that function’s execution; when a function’s execution ends, variables defined in its local scope are destroyed. But that’s not the case in JavaScript.
2.1 How are different scopes linked?
Let us understand this with an example:
let message = "Hello World"; function printMessage() { console.log(message); } printMessage();
When the printMessage
function is invoked, the local scope of this function is linked to the global scope using the hidden internal slot [[Environment]]
. This [[Environment]]
internal slot exists on the functions, and it contains a reference to the outer scope/environment. In other words, this internal slot contains a reference to the scope on which the containing function has closed over or formed a “closure.”
In the code example above, when the printMessage
function is defined in the global scope, the function object stores a reference to the global environment in its internal [[Environment]]
slot. When the function is later invoked, a new environment is created to execute the code within the function. This local environment is connected to the global environment by retrieving the value stored in the [[Environment]]
slot of the printMessage
function and saving it in an internal slot called [[OuterEnv]]
. Each environment object includes an internal slot that holds a reference to its outer environment. Let us revisit our earlier example:
let message = "hello"; function outerFunction() { function innerFunction() { console.log(message); } innerFunction(); } outerFunction();
In this sample code, there are three environments:
- The global environment
- The local environment of
outerFunction
function (created when the function is invoked) - The local environment of the
inner
function (created when the function is invoked).
This scope linkage can be shown with the help of following figure:
This scope chain is traversed by the JavaScript engine, if needed, to resolve the scope of any identifier. Thanks to the scope chain, a nested function can continue to access variables from its outer function, even after the outer function has finished executing. The outer environment remains in memory as long as the inner function retains a reference to it.
To summarize, every time a JavaScript function is created, a closure is formed, which allows that function to access the scope chain that was in effect when that function was defined. Each time a function is created, JavaScript saves the reference to the surrounding environment of the function in the internal [[Environment]]
slot on the function object. When that function is called, a new environment is created for that function call, and JavaScript saves the value of [[Environment]]
slot on the function in the [[OuterEnv]]
slot of the environment object.
It is a common misconception that closures are only formed when any function returns a nested function. But that is not the case. Every time a function is created in JavaScript, it forms a closure over the environment in which that function was created. Forming a closure is a fancy way of saying that when a function is created, it saves a reference to the environment in which it was created.
3. A practical example of Closure
A Closure is visible in the module pattern. Traditional function based modules can be used to demonstrate Closure. There are two conditions to create a module pattern:
- An outer enclosing function which is executed at least once.
- The outer enclosing function returns at least one inner function nested in outer function so that the inner function has closure over the inner scope of outer function.
function MyModule(msg) { function print() { console.log(msg); } return { printMg: print, }; } var module = MyModule("Hello World"); module.printMg(); // Hello World
In this code, MyModule()
returns print()
inside an object which can be used to access inner scope of MyModule
.
4. Closure in loop
Let us see a sample code:
for (var i = 1; i <= 3; i++) { setTimeout(() => { console.log(i); }, 1000); }
The output of this code is
4
4
4
You may expect that the output is
1
2
3
In this example, the callback function of each setTimeout
forms a closure over the same variable i
. In our example, since there are three loop iterations, setTimeout
is called three times, resulting in three callback functions, each forming a closure over the same variable i
. These callback functions are invoked after the loop has finished executing. By the end of the loop, the value of i is “4”. Since all the callbacks share the same closure over i, they all access the final value, which is why each one logs “4” to the console.
It’s important to understand that functions create closures over variables, not their values. This means that each function logs the most recent value of the variable it has closed over. If functions formed closures over the values instead of variables, they would capture and log the value as it was when the closure was created, rather than the updated value. In our example, if the closure had been over the values of i rather than the i variable itself, each callback would have logged the value of i as it was during the iteration in which the callback was created. This would have produced the expected output of “1 2 3” instead of “4 4 4”.
4.1 How to solve this problem
Before the introduction of ES2015, also known as ES6, one way to solve this problem was to use an IIFE (Immediately Invoked Function Expression).
for (var i = 1; i <= 3; i++) { ((counter) => { setTimeout(() => { console.log(counter); }, 1000); })(i); }
ES2015 introduced block-scoped variables and constants with the help of let
and const
keywords, respectively. We can solve the “closures in loop” problem simply by replacing the var
keyword with the let
keyword.
for (let i = 1; i <= 3; i++) { setTimeout(() => { console.log(i); }, 1000); }
Using the let
keyword solves this problem because, unlike each callback function closing over the same variable i
, the let
being block-scoped causes each iteration of the loop to have a different copy of the variable i
. This is the key idea that solves the problem of “closures in loop”. Each iteration has its own separate copy of variable i
, which means that the setTimeout
callback created in each iteration closes over the copy of variable i
that is limited to that particular iteration of the loop.
In our code example, we have three iterations of the loop and separate copies of variable i
, each limited to a particular iteration of the loop. Although it seems that we have a single variable i
, behind the scenes, each iteration gets its own copy of the variable i
. As each environment object created for each iteration of the loop has its separate copy of the variable i
, the closure of each callback created in different iterations of the loop forms a closure over a separate copy of the variable i
. As a result, they log the value they have closed over, giving us the expected output, i.e., 1 2 3.
5. Conclusion
Closures are a powerful feature in JavaScript that allow functions to retain access to variables from their lexical environment, even after those functions have returned. This concept enables various useful patterns such as data encapsulation, function factories, and maintaining private variables.
By understanding closures, you can write cleaner, more modular, and efficient code. Whether you’re building complex applications or simple utilities, closures give you the ability to manage scope and state effectively, making your JavaScript code more robust and maintainable.