DEV Community

Rae Liu
Rae Liu

Posted on

6 2

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

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

SurveyJS custom survey software

JavaScript UI Libraries for Surveys and Forms

SurveyJS lets you build a JSON-based form management system that integrates with any backend, giving you full control over your data and no user limits. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more.

Learn more

Top comments (7)

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 • Edited

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 • Edited

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 • Edited

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

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more