2

I am new to javascript, have mostly spent my time writing backend java program. I am unable to figure out how to capture the current state of the scope variables when calling then, example:-

idList = [0, 1, 2, 3, 4, 5];

for(id in idList) {
    new Promise(function(resolve, reject) { 
        // A mock async action using setTimeout
        setTimeout(function() {
            resolve(10); 
        }, 300);
    }).then(function(num) { 
        console.log(id + ': ' + num); 
        return num * 2; 
    });
}

The output is:-

"5: 10"
"5: 10"
"5: 10"
"5: 10"
"5: 10"
"5: 10"

But I want the output to be

"0: 10"
"1: 10"
"2: 10"
"3: 10"
"4: 10"
"5: 10"

i.e want then to capture the current state of variable id.

Nick Zuber
  • 5,467
  • 3
  • 24
  • 48
user1918858
  • 1,202
  • 1
  • 20
  • 29

6 Answers6

1

There are a few solutions to this.

The easiest to implement (given the code you provided), is to simply put a let before id in your for loop.

for (let id in idList) { ... }

Since the for...in loop you are using is asynchronous, it is completing before the inner functions contained within the loop are returning, thus id will always be whatever it was when the loop finished (in this case, 5).

The reason let must be used instead of var is because the let statement declares a block scope local variable.

let allows you to declare variables that are limited in scope to the block, statement, or expression on which it is used. This is unlike the var keyword, which defines a variable globally, or locally to an entire function regardless of block scope. (source)

So, let will work, var (or a global variable, as you have it) will not.

However, it is advised not to use for...in loops on arrays when the index order is important. (source)


A better solution would be to use the for...of loop, which, will visit the elements in a consistent order. This is also a fairly simple change of swapping out the word "in" with "of" to create the loop.

for (let id of idList) { ... }

A more declarative solution would be to use the forEach method available on every array created in JavaScript. This is executes a callback function for each element in the array.

const idList = [0, 1, 2, 3, 4, 5];

idList.forEach(id => {
    new Promise((resolve, reject) => { 
        setTimeout(() => resolve(10), 300);
    })
    .then((num) => { 
        console.log(id + ': ' + num); 
        return num * 2; 
    });
})

However, please note that forEach will always return undefined, even if you give the callback a return value. That doesn't seem to be a problem in your case here, since all you are looking to do is console.log the asynchronous values as they come in.

indiesquidge
  • 781
  • 2
  • 8
  • 14
0

So the issue here is with you using for...in and trying to iterate over the array that way. This brings up issues with the scoping of id and by the time your function executes, the value of id is already 5.

Since the issue is with scoping, as @wrleskovec mentions in the comments we can resolve this issue by simply changing

for (id in idList)

to

for (let id in idList)

However, we should avoid using for...in for iterating over arrays.

We can fix this by encapsulating id within a function by using a forEach on the array instead.

idList = [0, 1, 2, 3, 4, 5];

idList.forEach((id) => {
    new Promise(function(resolve, reject) { 
        // A mock async action using setTimeout
        setTimeout(function() {
            resolve(10); 
        }, 300);
    }).then(function(num) { 
        console.log(id + ': ' + num); 
        return num * 2; 
    });
});

Also as a side note, return num * 2; currently doesn't do anything — not sure if this is intentional or not.

Community
  • 1
  • 1
Nick Zuber
  • 5,467
  • 3
  • 24
  • 48
0

Since the method inside the loop is asynchronous, you will have to use either:

IIFE

Immediately-Invoked Function Expression captures the current state so that your code would look like this.

for(var id in idList) {

    (function(_id){
        new Promise(function(resolve, reject) { 
            // A mock async action using setTimeout
            setTimeout(function() {
                resolve(10); 
            }, 300);
        }).then(function(num) { 
            console.log(_id + ': ' + num); 
            return num * 2; 
        });
    }(id);
}

Async Module

Once you have understood IIFE, you can start using a module that easily handles asynchronous methods like async

Using this, your code would look like:

async.each(idList, function(id, callback) {

    new Promise(function(resolve, reject) { 
            // A mock async action using setTimeout
            setTimeout(function() {
                resolve(10); 
            }, 300);
        }).then(function(num) { 
            console.log(id + ': ' + num); 
            callback(null, num * 2);
            return num * 2; 
        });
}, function(err, results) {

    console.log('done with results %o', results);

});

I hope this helps!

Community
  • 1
  • 1
Taku
  • 5,639
  • 2
  • 42
  • 31
0

The problem is the scoping of your for loop variable. The for loop variable is in the parent function or global scope. You can use a block scoped variable like let in ES6 or you would need to create a new variable scope by wrapping your loop execution into a function. You should read up about how javascript scope and closures work.

The easiest solution IMO would be:

    for(let id in idList) {
    new Promise(function(resolve, reject) { 
        // A mock async action using setTimeout
        setTimeout(function() {
            resolve(10); 
        }, 300);
    }).then(function(num) { 
        console.log(id + ': ' + num); 
        return num * 2; 
    });
}

But if you are working in a strictly pre-ES6 environment you will need to wrap the looped execution context in a function to preserve the loop iterator in scope.

wrleskovec
  • 326
  • 1
  • 7
0

This is another approach

idList = [0, 1, 2, 3, 4, 5];

idList.reduce((p, id) => {
  return p.then(function(id, value) {
    console.log("%s : %s", id, value);
    return new Promise(resolve => 
      setTimeout(() => resolve(10), 300)
    );
  }.bind(null, id));
}, Promise.resolve(10));
Tolgahan Albayrak
  • 3,118
  • 1
  • 25
  • 28
0

you can try this

idList = [0, 1, 2, 3, 4, 5];

for(id in idList) {
    new Promise(function(resolve, reject) { 
        // A mock async action using setTimeout
        //save id value
        var idValue = id;
        setTimeout(function() {
            resolve({idValue: idValue, num: 10}); 
        }, 300);
    }).then(function(obj) { 
        console.log(obj.idValue + ': ' + obj.num); 
        return obj.num * 2; 
    });
}