JS callback and loops

Dealing with repeated values when calling callbacks inside a loop in JavaScript

Often using callbacks and loops, it happens that we get repeated value for one variable, generally the last one. For example if you try to call a function (callback) using an array inside a for loop, you will end up calling it with the same data. Consider this code:

var arr = [1,2,3,4,5,6,7,8,9];
for (var i = 0; i < arr.length; i++) {
  document.getElementById('button'+i).addEventListener('click', function () { console.log('You clicked button '+i); });
}

Now, when you will click the corresponding buttons, you will always get You clicked button 9 in the console.

Same thing will happen for the following code:

function fileSelectHandler(e) {
  var files = e.target.files || e.dataTransfer.files;
  for (var i = 0; i < files.length; i++) {
    readFileContent(files[i]).then(content => {console.log(content);});
  }
}
function readFileContent(file) {
  const reader = new FileReader()
  return new Promise((resolve, reject) => {
    reader.onload = event => resolve(event.target.result)
    reader.onerror = error => reject(error)
    reader.readAsText(file)
  });
}

You will be surprised to see that the console logs same content for all files.

So, why does this happen?

This happens because of the scopes and lifetimes of variables. By the time the first callback executes, the value of the scoped-variable changes to the last iteration of the loop. And this value is used for all the callbacks.

To avoid this, we can use an immediately-invoked function expression. The syntax would be like:

(function(index) {
  ///actual work here
} (i);

Here, all the variables that you need to use in the function which vary for each iteration/callback, should be declared in the function parameters (probably with different names) and then can be passed to the function using parentheses.

So, the above examples could be re-written as:

var arr = [1,2,3,4,5,6,7,8,9];
for (var i = 0; i < arr.length; i++) {
  function (index) {
    document.getElementById('button' + index).addEventListener('click', function () { console.log('You clicked button '+index); });
  } (i);
}

and the second example could be re-written as:

function fileSelectHandler(e) {
  var files = e.target.files || e.dataTransfer.files;
  for (var i = 0; i < files.length; i++) {
    (function (file) {
      readFileContent(file).then(content => {console.log(content);});
    } (files[i]);
  }
}

And, now these would work as intended.

If you use the same names for the variables inside the immediately-invoked function call, variable shadowing would take place and you would get the same result.

You can pass any number of variables this way simply by adding them to the function call and then passing them later as arguments to the function.

A quick tip: Try to use let or const instead of var while creating a new variable in a loop body. This will help you to minimize such problems in the long run.

One StackOverflow answer also explains three approaches to solve this problem: ES6 way (using let), ES5.1 way (using forEach) and the classic closure approach.

All of these approaches have their own mechanisms to tackle the problem. For example the closure approach is nothing but defining the immediately invoked function separately and then calling the function with same parameters.