13

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?

4 Answers 4

15

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.

Sign up to request clarification or add additional context in comments.

2 Comments

What if I have 2 parameters (which I am not supposed to pass) other than index? For example: function(result, status). How can we use bind then?
You basically use it like Function.prototype.call. So you'd do fn.bind(context, firstParam, secondParam, thirdParam) etc.
5

The setTimeout doesn't have a snapshot of index as you are expecting. All of the timeouts will see the index as the final iteration because your loop completes before the timeouts fire. You can wrap it in a closure and pass index in, which means the index in the closure is protected from any changes to the global index.

(function(index){
    setTimeout(function(){
        self.arr[index] = 'I am John!';
    }, 100);
})(index);

Comments

0

The reason is that by the time settimeout is started, the for loop is finished executing the index value is 9 so all the timers are basically setting the arr[9].

Comments

-2

The previous answer is correct but the source code provided is wrong, there is a mistyping elf in place of self . The solutions works.

An other way , without a closure , is to just add the index parameter to the function declaration in the setTimeout statement

function Demo() {
    this.arr = new Array();
    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!';
            }(index), 100);
        }
    };
    this.logMember = function() {
        console.log(this.arr);
    };
}

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

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

2 Comments

This is incorrect and does not work. You will invoke the function directly, not after 100 ms. You will pass it an argument but it will be thrown away. Then index is taken from the outer scope, which is fine because it executes synchronously. Then after 100ms nothing happens.
If you cut-paste the code in the firefow scratchpad , it's display 9th I'am john, which is the initial question. I just want to help for the mistype.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.