-10

I am writing a function to fire a series of click handlers with a given time interval in between.

function play(step){
    var buttons = document.querySelectorAll('.age');
    buttons[0].click();
    for (var i = 1; i < buttons.length; i++){
        setTimeout(function(b){
            b.click();
        }, 300 + (step*i), buttons[i]);
    }
}

The above function works as intended, however, the following version which to me seems like it should be equivalent, does not work:

function play(step){
    var buttons = document.querySelectorAll('.age');
    buttons[0].click();
    for (var i = 1; i < buttons.length; i++){
        setTimeout(function(b){
            console.log(i);
            console.log(b);
            b[i].click();
        }, 300 + (step*i), buttons);
    }
}

I was getting the following error:

 TypeError: Cannot read property 'click' of undefined

After checking, I find that console.log(i) is printing 6. So, apparently, the attribute access isn't occurring until after the loop is over, which explains the error! But what exactly is going on here? I'm relatively new to javascript, but is this behavior the result of the anonymous function acting as a closure? That doesn't sound like the right explanation to me. Is it because setTimeout is delaying the evaluation of the anonymous function?

ETA:

I did an experiment:

function sleep(ms){
    var current = new Date().getTime();
    while (current + ms >= new Date().getTime()){};
}
function play(step){
    var buttons = document.querySelectorAll('.age');
    buttons[0].click();
    for (var i = 1; i < buttons.length; i++){
        setTimeout(function(b){
            console.log(b);
            console.log(i);
            b[i].click();
        }, 300 + (step*i), buttons);
        sleep(1000);
    }
}

when I run play(200) I get the same error message, so sleep(1000) should be enough time to make sure that the loop hasn't exited before the first timeout is up, no?

Towkir
  • 3,889
  • 2
  • 22
  • 41
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172

1 Answers1

2

It's because variable in for loop is the same in each iteration (same reference) and when setTimeout is running the for loop has ended so the i variable will be last value of the loop, to fix this you can create a closure with i variable:

function play(step){
    var buttons = document.querySelectorAll('.age');
    buttons[0].click();
    for (var i = 1; i < buttons.length; i++){
        (function(i) {
            setTimeout(function(b){
                console.log(i);
                console.log(b);
                b[i].click();
            }, 300 + (step*i), buttons);
        })(i);
    }
}
jcubic
  • 61,973
  • 54
  • 229
  • 402
  • Will that cause `Cannot read property 'click' of undefined` ? `b[i]` should point the last element in the `NodeList` not `undefined`! – Rayon Apr 27 '16 at 08:11
  • @Rayon the i variable will be increased at the end of the loop, so it will be equal to `buttons.length` which will cause off by one error. – jcubic Apr 27 '16 at 08:16
  • Ooh yes! Got it.. Thanks :) – Rayon Apr 27 '16 at 08:18