views:

1250

answers:

2

I have a piece of jQuery code which invokes several getJSON() calls in quick succession:

var table = $("table#output");
for (var i in items) {
    var thisItem = items[i];
    $.getJSON("myService", { "itemID": thisItem }, function(json) {
        var str = "<tr>";
        str += "<td>" + thisItem + "</td>";
        str += "<td>" + json.someMember + "</td>";
        str += "</tr>";
        table.append(str);
    });
}

When I run this against a laggy server, the table gets populated with the expected json.someMember values (they arrive out of order: I don't mind that), but the thisItem column is populated with an unpredictable mixture of values from various iterations.

I'm assuming this is something to do with scope and timing - the callback function is reading thisItem from a wider scope? Am I right? How do I prevent this?

My current workaround is for the JSON service to return a copy of its inputs - which is unsatisfying to say the least.

+10  A: 

Seems like a scoping issue due to the loop. Try this:

var table = $("table#output");
for (var i in items) {
    var thisItem = items[i];
    $.getJSON("myService", { "itemID": thisItem }, (function(thisItem) {
        return function(json) {
            var str = "<tr>";
            str += "<td>" + thisItem + "</td>";
            str += "<td>" + json.someMember + "</td>";
            str += "</tr>";
            table.append(str);
        }
    })(thisItem));
}

Edit: all I did was scope thisItem to the $.getJSON callback.

Crescent Fresh
I don't think this would work with jQuery. Wouldn't the json end up getting passed into the closure instead of the thisItem variable and cause an error?
jacobangel
No. $.getJSON gets passed the inner function; the one that does get the "json" parameter.
Crescent Fresh
Seems to have worked a treat (after I got all the braces sorted!). Thanks!
slim
Thanks Crescent Fresh, had to pass in the index for the image table I wanted to populate (got burned by asynchronous returns, even though I knew about them wasn't sure how to scope it). Passing the index worked like a charm!
Mark Essel
+3  A: 

Javascript does not use block for scope. Scope is only based on functions.

If you want a new scope, you have to declare a new internal function and run it immediately, this is the only way to create a new scope in Javascript.

var table = $("table#output");
for( var i in items ) 
{
    (function(){
        var thisItem = items[i];
        $.getJSON("myService", { "itemID": thisItem }, function(json) 
     {
            var str = "<tr>";
            str += "<td>" + thisItem + "</td>";
            str += "<td>" + json.someMember + "</td>";
            str += "</tr>";
            table.append(str);
     });
    })();
}
Vincent Robert
I like this solution since it allows me to have more local variables like thisItem, without a verbose function signature.
slim
fun fact: this is how scoping is usually implemented in Scheme
Javier
I forgot another way, you could use a for loop from a library, these actually take a function as the block to execute at each iteration. In jQuery: $.each(items, function(i, item){ ... });
Vincent Robert
Thanks a bunch Vincent. I was getting burned by scope on asynchronous callbacks. encapsulated a passed index to know where to put my returns and whala!
Mark Essel