Closures are one of the most intriguing concepts in Javascript. I've been referring to a ton of resources for clarification. For me what worked was EJS and Namasthe Javascript. When I combined these two everything fell into place. Let's dive into dissecting closures. I'll be using the same example as in Namasthe JS with some refactoring for better understanding.
Here's the code that we are going to mess around with.
function outer() {
for(var i=1; i<=5; i++) {
setTimeout(() => {
console.log(i)
}, i*1000)
}
console.log("I run first");
}
outer();
Take a moment to think about the output, get your neurons to start firing and then run the code in your fav IDE. Spoiler Alert! The output is going to be 'I run first' and then '6' logged 5 times in the console at the end of 5 seconds. '6' is logged each second as soon as you run the code. But, why '6'? why isn't it logging '1', '2', '3', '4', '5' after each second? The culprit is the var
keyword in for
loop.
The concepts at play here are scope of var
, function call, async programming, closures and references. We'll go through it one by one. Firstly, it is the async behaviour of setTimeout
, which is why 'I run first' gets logged in before '6'.
For a moment imagine, there's no outer()
function, there's only a for
loop. Here i
will be added to window object and its value will change as the loop runs. setTimeout
is run 5 times asynchronously and the callback function exist in parallel but with different timers. i*1000
gets resolved when for loop runs, but console.log(i)
is still dormant. So, when the timer runs out the value of i
has been changed and all these 5 copies of callback functions points to the same i
which exists in the window object. And that i
is '6' now.
Now let the outer()
function exist and we call it. In the current situation 'var' scope is limited to outer()
, since its function scoped. We run the program, outer()
is called, for
loop is run, same thing happens as above but i
is not added to window object rather its in the local scope of outer()
. So now when the timer runs out for the first setTimeout
of 1 second, callback
function wakes up from its sleep and finds i
in closure formed with outer()
function.
Closure is basically a function and its environment that's created each time a function is called. This environment has reference to the instance of bindings of its parent and above when the function was created. This feature helps functions to get access to bindings in its outer scope. To put it in different terms, a child function knows the assets(bindings/variables) owned by its parent, grandparent and so on. Whenever a child is called, it recalls its roots if need be.
Now i
is up for grabs with the closure formed with outer()
. Remember outer()
is called just once and therefore there's only one instance of this environment. Even in this environment i
is changed by for
loop. So all the copies of callback
function points to i
which is already '6'. All because of the powerful scope of var
.
Our goal is to get the output as '1', '2', '3', '4', '5'. For that we need a new instance of i
for every step of the for
loop. In 2015, a new keyword let
was introduced in JS which is block scoped. Since the block is run every time the loop condition is met, a new instance of i
is created for every block with references to different values. Here one should note that, there are 5 blocks existing simultaneously, with different values of i
. So when the timer runs out, the callback remembers its block environment which has a particular value of i
, kind of a closure but there is no parent function involved. Hence we'll get the desired output.
What if we don't want to use let
but want to continue with var
. Here the idea is to hijack value of i
and call a function each time the loop is run, so that we have a new closure environment with each value of i
.
function outer() {
for(var i=1; i<=5; i++) {
function inner(a) {
setTimeout(() => {
console.log(a)
}, a*1000)
}
inner(i);
}
console.log("I run first");
}
outer();
As per the above code, the value of i
is taken over by a
each time the loop runs. Since the function inner()
is called 5 times, there are 5 closures with inner()
existing in parallel which have different values of i
captured by a
.
Now 5 timers have started ticking at almost the same time. First, the timer for 1 second ends, callback
function wakes up from its dormant state to find a
. But where is a
? a
is in the closure formed with inner()
function where a
was set to '1'. To put it in GOT words, the callback function remembers. It knows a
was 1, when the timer was set to 1 second . Remember there's 5 different copies of the callback function and 5 closure instances of a
with values '1', '2', '3', '4', '5'. Each copy gets called when its timer runs out.
I would suggest you to use the debugger in your browser to thoroughly understand the workings of this. I have attached some screenshots for reference for each method i.e var
, let
and var
with a inner function. In the end it's all magic by JS.
Comments/Corrections are welcome.
Top comments (0)