Previously I gave a brief introduction to closures in Javascript. Here I will attempt to provide more understanding on this topic. I will lead into the topic of closures by beginning with a look at an infamous problem with for loops in Javascript. I will also briefly touch on Immediately Invoked Function Expressions (IIFEs).
Consider the following code snippet.
var arrayOfFunctions = []; function buildFunctions() { for (var i = 0; i < 3; i++) { arrayOfFunctions.push(function(){ console.log(i); }); } return arrayOfFunctions; } // fs will now be an array that contains a function as each of its items var fs = buildFunctions(); // fs[0] will refer to the first array item and fs[0]() will call that first item because, being a function, it is callable. fs[0](); fs[1](); fs[2]();
- The
buildFunctions()
function will add an item to the end of the array. In this case, the item added will be a function. - The definition for the function specifies to log what ever the value of
i
is when you run the function later on, not what the current value ofi
is at the time the loop is running.
In the first iteration of the loop, when we come across the following…
arrayOfFunctions.push(function() { console.log(i); });
Here we are adding a function to the array which will log the value of i
, and because the value of i
is currently 0 at that point (then 1, then 2 etc), we might think we are telling the function to log the value of 0, but we are actually telling it to log the value of i
, whatever that value will be at some point in the future, not what it currently is right now as the loop is running. It not only sounds confusing but LOOKS confusing.
Look at this simpler example.
for (var i = 0; i < 3; i++) { console.log(i); // log what i currently is, at this point in the loop, right now } /* logs: 0 1 2 */
Now compare it with the following.
for (var i = 0; i < 3; i++) { alert(i); } console.log(i); // logs 3
Here we have moved the log statement out of the loop and just replaced it with an alert.
The alert will display 1, 2, 3 respectively, but what will the log statement show?
It will show 3 because by this stage the loop has finished running and the final value of i
will be equal to 3.
Looking back at our original code…
arrayOfFunctions.push(function() { console.log(i); // console.log statement doesn't run now but later });
The console.log
function does not run at that time, in other words, it does not run during the execution of the for loop, so it does not matter what the current value of i
is at that point in the loop because the log function is not being run then. When the log function is finally run later (by calling its containing function as an item of the array) it will log whatever i
happens to be then, which in our example will be 3, because by then the loop has finished running.
So how do we get the functions to log the different index numbers? The solution is to use a closure.
var arrayOfFunctions = []; function buildFunctions() { for (var i = 0; i < 3; i++) { arrayOfFunctions.push( (function(currentValueOfI) { return function(){ console.log(currentValueOfI); } }(i)) // end of IIFE ) // end of push statement } // end of for loop return arrayOfFunctions; }
On each iteration of the loop we add an item to the array, as we did before. The item that is added will be the result of running the IIFE. The IIFE returns a function, so the item added to the array will obviously be a function. In the first iteration the value of i
is equal to 0. The IIFE will call itself with i
(0) as its argument. That value will be supplied to the parameter/placeholder currentValueOfI
.
The IIFE returns a function that will later log that argument which was supplied (in this case, 0). So when we later call arrayOfFunctions[0]()
, it will “remember” that at the time we defined that function as an array item, the value of i
was equal to 0, because we captured it in another variable called currentValueOfI
.
On the second iteration the value of i (now equal to 1) will be once again captured in another variable called currentValueOfI
.
But won’t that clash with, or overwrite, the previous variable called currentValueOfI
which was set to 0?
No, because each time the loop iterates, it runs the same IIFE, but each execution of that IIFE creates its own execution context (its own scope, in other words), so there will be more than one variable called currentValueOfI
but they will be separately contained in their own scope/execution context.
Look at the following alteration of our original example and read the comments closely.
// create a global variable called j var j = 25; var arrayOfFunctions = []; function buildFunctions() { for (var i = 0; i < 3; i++) { arrayOfFunctions.push(function(){ // log j instead of i. Shows that the current value of i in the loop is irrelevant to the function we are adding to the array. It couldn't care less. console.log(j); }); } return arrayOfFunctions; } var fs = buildFunctions(); fs[0](); // logs 25 fs[1](); // logs 25 // modify (update) j j = 50; // only cares what current value of j is fs[2](); // logs 50