1

I have a problem with calling a function with a parameter inside a setTimeout function. Basically I'm trying to make a small online game, where I create a queue of commands and then execute them one at a time (each takes some time to show a visualization).

Unfortunately it seems that I cannot pass any variable as a parameter inside the setTimeout(). Although the variable does exist when I call the function it does not exist later when it is executed. The function doesn't keep track of the passed value.

Is there any solution to this? Thanks a lot for any help. Here is a code I use:

function executeCommands() {
    var commands = document.getElementsByClassName("cmdplace");
    var timeout = 0;
    for (i = 0; i < commands.length; i++) {
        console.log(commands[i].childNodes[0]); //variable exists
        setTimeout(function() {go(commands[i].childNodes[0]);}, timeout+=400);  //Uncaught TypeError: Cannot read property 'childNodes' of undefined
        console.log(commands[i].childNodes[0]); //variable still exists
    }
}

function go(command) {
    //do somethig based on the passed command
}
Petr Hofman
  • 162
  • 1
  • 15

2 Answers2

2

When your functions are invoked, i is equal to commands.length and commands[i] is undefined.
They are capturing the variable i, not its value.
When they execute, they get out of i the actual value, but so far it has reached commands.length (that is the condition used to break your loop).

You can do something like this to work around it:

setTimeout(function(j) {
    go(commands[j].childNodes[0]);
}.bind(null, i), timeout+=400);

Or this:

setTimeout((function(j) {
    return function() {
        go(commands[j].childNodes[0]);
    };
})(i), timeout+=400);

Note also that, as you defined it, i is a global variable.


As mentioned in the comments by @PMV, there's a much easier way in modern JavaScript (if that's an option for you).
Just use a let statement as it follows:

for (let i = 0; i < commands.length; i++) {
    // do whatever you want here with i
}

This will ensure that each iteration gets a new variable named i and you can capture it as in the original code.

skypjack
  • 49,335
  • 19
  • 95
  • 187
  • There's a much easier way with ES6: just use a `let` statement: `for (let i = 0; i < commands.length; i++) {`. This will ensure each iteration gets a new variable named `i` to capture. – PMV Nov 26 '16 at 01:45
  • @PMV Right. I'm adding the comment with a mention to the answer. Thank you. – skypjack Nov 26 '16 at 08:51
  • @Petr hofman please read this https://community.risingstack.com/explaining-javascript-closure-scope-chain-examples/ – hod caspi Nov 26 '16 at 10:28
0

You need to make a distinct copy of each item. By the time the setTimeout runs the loop has already finished.

var timeout = 0;

function executeCommands() {
    var commands = document.getElementsByClassName("cmdplace");
    
    for (i = 0; i < commands.length; i++) {
          go(commands[i]);
    }
}

function go(command) {
  setTimeout(function() {
     console.log(command);
  }, timeout += 400);  
}

executeCommands();
<ul>
  <li class="cmdplace">A</li>
  <li class="cmdplace">B</li>
  <li class="cmdplace">C</li>
  <li class="cmdplace">D</li>
</ul>
Stubbies
  • 3,054
  • 1
  • 24
  • 33