What will be the output of this script?
const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log(i + '- element: ' + arr[i]);
}, 100);
}
//desired output
//1- element: 10
//1- element: 12
//1- element: 15
//1- element: 21
//actual output
//4 - element: undified
//4 - element: undified
//4 - element: undified
//4 - element: undified
There are two reasons why it doesn't work as expected -
- JavaScript is a synchronous programming language
- Each loop is sharing the same
i
variable that is outside the function
All loops are running simultaneously and the i
keeps increasing until it hits arr.length - 1
.
To fix the issue, we need to change i
from a global variable to a local variable.
Solution 1 - use IIFE (Immediately Invoked Function Expression)
An IIFE is a JavaScript function that runs as soon as it is defined, and the variable within the expression can not be accessed from outside it(1).
for (var i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log('Index: ' + i + ', element: ' + arr[i]);
}(), 100);
}
Note: Solution 1 will invoke function immediately regardless of time delay, which means the code above won't work on setTimeout
.
You can still use IIFE in setTimeout
, and here is the code below. Thanks JasperHorn!
for (var i = 0; i < arr.length; i++) {
setTimeout(function (i) { return function() {
console.log('Index: ' + i + ', element: ' + arr[i]);
}}(i), 100);
}
Solution 2 - for
can be replaced by forEach
to avoid global i
i
in forEach
- The index of the current element being processed in the array(2).
Note: forEach
is included in ES5
arr.forEach(function(element, i){
setTimeout(function(){
console.log('Index: ' + i + ', element: ' + element);
}, 100)
})
Solution 3 - change var
to let
let
allows to declare variables in a local scope, so each function can use its own i
value.
Note: let
is included in ES6
for (let i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log(i + '- element: ' + arr[i]);
}, 100);
}
Top comments (7)
I'm not sure the code for example 1 is correct.
The code as currently written, logs things right away and then when the timeout is over, it does nothing. This isn't too obvious when the timeout is 100ms, but it becomes a lot clearer when you set a larger timeout. (With an extra
function (){
before the function and an extra}
that type of solution can be made to work nonetheless.Thank you Jasper! Yes, it doesn't make sense to use IIFE for setTimeout. I am going to add a note on Solution 1.
Note that you can use it, though it gets pretty hard to read in this situation:
There might be situations where that makes sense. I wouldn't know exactly which ones that would be (though I'm pretty sure the situation starts with "I can't use
let
").Solution 4: use the closure from the
setTimeout
callback:Warning: does not work in Internet Explorer.
Thank you Alex, I was thinking about setTimeout callback! For other functions, is it better to create a separate callback function?
I only added it for the sake of completeness.
While I really like the ability to control scope in ES6 and therefore would probably prefer to use
let
, for most loops, I would be usingforEach
or one of its brethren methods in any case, so usinglet
instead would not improve legibility.or you do that:
anyway, all of them work perfectly