DEV Community

loading...

Pass parameter to setTimeout inside a loop - JavaScript closure inside a loop

mingyena profile image Rae Liu ・2 min read

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
Enter fullscreen mode Exit fullscreen mode

There are two reasons why it doesn't work as expected -

  1. JavaScript is a synchronous programming language
  2. 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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

References

  1. https://developer.mozilla.org/en-US/docs/Glossary/IIFE
  2. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

Discussion

pic
Editor guide
Collapse
jasperhorn profile image
JasperHorn

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.

Collapse
mingyena profile image
Rae Liu Author

Thank you Jasper! Yes, it doesn't make sense to use IIFE for setTimeout. I am going to add a note on Solution 1.

Collapse
jasperhorn profile image
JasperHorn

Note that you can use it, though it gets pretty hard to read in this situation:

for (var i = 0; i < arr.length; i++) {
  setTimeout(function (i) { return function() {
    console.log('Index: ' + i + ', element: ' + arr[i]);
  }}(i), 100);
}

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").

Collapse
lexlohr profile image
Alex Lohr

Solution 4: use the closure from the setTimeout callback:

for (var i = 0; i < arr.length; i++) {
  setTimeout(function(index, element) {
    console.log('Index: ' + index + ', element: ' + element);
  }(), 100, i, arr[i]);
}

Warning: does not work in Internet Explorer.

Collapse
mingyena profile image
Rae Liu Author

Thank you Alex, I was thinking about setTimeout callback! For other functions, is it better to create a separate callback function?

Collapse
lexlohr profile image
Alex Lohr

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 using forEach or one of its brethren methods in any case, so using let instead would not improve legibility.

Collapse
laphilosophia profile image
Erdem Arslan

or you do that:

for (let i = 0; i < arr.length; i++) {
  (e => {
    setTimeout(() => {
      return console.log(e)
    }, 100)
  })(i)
}

anyway, all of them work perfectly