Joshua Soileau
Joshua Soileau

Reputation: 3025

Javascript For ... In with an Object, Always Refers to the Last Key in the Object

I have this code:

var obj = {"a" : "A", "b" : "B", "c" : "C"};
for( value in obj) {
    setTimeout(function() { 
        console.log(value); 
    }, 100);
}

The result of running that code in your console is that c is logged three times.

Why does it always refer to the last key in the setTimeout(), and how do I get it to refer to them in order inside my for loop?

It's got something to do with scope, but I can't wrap my head around it...

Upvotes: 0

Views: 54

Answers (3)

Thayne
Thayne

Reputation: 7012

You have a single variable value which is set to an element of obj for each element in obj. Your closure has a reference to that variable, so when value changes in the enclosing scope, it also changes in the closure. Therefore, once the closure is executed after 100 milliseconds, value has been assigned to the last item in obj. To fix this, you need to introduce a new scope by adding another anonymous function, which you pass the value you need to store temporarily:

var obj = {"a": "A", "b": "B", "c":"C"};
for( var value in obj) {
    (function(value) {
         setTimeout(function() {
             console.log(value);
         },100);
     })(value);
 }

or:

var obj = {"a": "A", "b": "B", "c":"C"};
for(var value in obj) {
    (function() {
         var value2 = value;
         setTimeout(function() {
             console.log(value2);
         },100);
     })();
 }

You need the paranthesis around the function definition because if "function" is the first token on a line, javascript will treat it as a named function declaration, not a function expression.

Using a closure is the standard way to introduce a new scope in Javascript as of ECMAScript 5. However, in ECMAScript 6 (which is not yet finalized or implemented), you could use a let statement instead.

p.s. You should probably declare value as a var, unless you have done so elsewhere in your function, otherewise you will create a global variable.

Upvotes: 0

Stuart
Stuart

Reputation: 9868

An alternative, and sometimes cleaner, way of creating the closure is to make a separate function that returns a timeout function based on the set value:

function getTimeoutFunction(value) {
    return function() {
        console.log(value);
    };
}
for (value in obj) {
    setTimeout(getTimeoutFunction(value), 100);
}

Upvotes: 2

Tom Leese
Tom Leese

Reputation: 19729

You need to use a closure.

var obj = {"a" : "A", "b" : "B", "c" : "C"};
for( value in obj) {
    (function(value) {
        setTimeout(function() { 
           console.log(value); 
        }, 100);
    }(value));
}

This is because the value of value will have changed by the time the setTimeout callback function will be called.

By wrapping it in an anonymous function the value of value won't change because a new function will be created for each item in the object — JavaScript has function scope rather than the traditional block scope.

Upvotes: 3

Related Questions