Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript: Using changing global variables in setTimeout

Tags:

javascript

I am working on Javascript and using firefox scratchpad for executing it. I have a global index which I want to fetch inside my setTimeout (or any function executed asynchronously). I can't use Array.push as the order of data must remain as if it is executed sequentially. Here is my code:-

function Demo() {
    this.arr = [];
    this.counter = 0;
    this.setMember = function() {
        var self = this;

        for(; this.counter < 10; this.counter++){
            var index = this.counter;
            setTimeout(function(){
                self.arr[index] = 'I am John!';
            }, 100);
        }
    };
    this.logMember = function() {
        console.log(this.arr);
    };
}

var d = new Demo();
d.setMember();

setTimeout(function(){
    d.logMember();
}, 1000);

Here, I wanted my d.arr to have 0 - 9 indexes, all having 'I am John!', but only 9th index is having 'I am John!'. I thought, saving this.counter into index local variable will take a snapshot of this.counter. Can anybody please help me understand whats wrong with my code?

like image 810
Prakhar Mishra Avatar asked Apr 25 '14 13:04

Prakhar Mishra


People also ask

Is setTimeout global?

A string passed to setTimeout() is evaluated in the global context, so local symbols in the context where setTimeout() was called will not be available when the string is evaluated as code.

What is the alternative for setTimeout in JavaScript?

The setInterval method has the same syntax as setTimeout : let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...) All arguments have the same meaning. But unlike setTimeout it runs the function not only once, but regularly after the given interval of time.

Is setTimeout deprecated in JavaScript?

We all know that passing a string to setTimeout (or setInterval ) is evil, because it is run in the global scope, has performance issues, is potentially insecure if you're injecting any parameters, etc. So doing this is definitely deprecated: setTimeout('doSomething(someVar)', 10000);


1 Answers

The problem in this case has to do with scoping in JS. Since there is no block scope, it's basically equivalent to

this.setMember = function() {
    var self = this;
    var index;

    for(; this.counter < 10; this.counter++){
        index = this.counter;
        setTimeout(function(){
            self.arr[index] = 'I am John!';
        }, 100);
    }
};

Of course, since the assignment is asynchronous, the loop will run to completion, setting index to 9. Then the function will execute 10 times after 100ms.

There are several ways you can do this:

  1. IIFE (Immediately invoked function expression) + closure

    this.setMember = function() {
        var self = this;
        var index;
    
        for(; this.counter < 10; this.counter++){
            index = this.counter;
            setTimeout((function (i) {
                return function(){
                    self.arr[i] = 'I am John!';
                }
            })(index), 100);
        }
    };
    

    Here we create an anonymous function, immediately call it with the index, which then returns a function which will do the assignment. The current value of index is saved as i in the closure scope and the assignment is correct

  2. Similar to 1 but using a separate method

    this.createAssignmentCallback = function (index) {
        var self = this;
        return function () {
             self.arr[index] = 'I am John!';
        };
    };
    
    this.setMember = function() {
        var self = this;
        var index;
    
        for(; this.counter < 10; this.counter++){
            index = this.counter;
            setTimeout(this.createAssignmentCallback(index), 100);
        }
    };  
    
  3. Using Function.prototype.bind

    this.setMember = function() {
        for(; this.counter < 10; this.counter++){
            setTimeout(function(i){
                this.arr[i] = 'I am John!';
            }.bind(this, this.counter), 100);
        }
    };
    

    Since all we care about is getting the right kind of i into the function, we can make use of the second argument of bind, which partially applies a function to make sure it will be called with the current index later. We can also get rid of the self = this line since we can directly bind the this value of the function called. We can of course also get rid of the index variable and use this.counter directly, making it even more concise.

Personally I think the third solution is the best. It's short, elegant, and does exactly what we need. Everything else is more a hack to accomplish things the language did not support at the time. Since we have bind, there is no better way to solve this.

like image 107
Tim Avatar answered Oct 14 '22 09:10

Tim