ben
ben

Reputation: 29777

Javascript variable problem

I have an array of strings in Javascript that I need to use to load images in my page via AJAX. After each image is loaded, I need to perform some additional tasks such as sending a HTTP request which will actually delete the image.

This is what I have so far.

for (x in images) {
    var img = new Image();
    $(img)
        .load(function(){
            $('#image_'+images[x]).html(''); //remove loading gif
            $('#image_'+images[x]).append(this);
            $.post('/images/destroy_image',{id:images[x]});
        })
        .attr('src', '/images/get_image?id='+images[x]);
}

This code works fine if there is only 1 string in the images array.

If there is more than 1, when an image finishes loading and its load function is run, any reference to images[x] in its load function now point to the final string in the images array, when I need it to be the value it was when the loop was being run.

Eg.

images is {'12','100'}

When the first image finishes loading, the first line in its load function will be run as

$('#image_100').html('');

when it should be

$('#image_12').html('');

How can I do this?

Upvotes: 0

Views: 91

Answers (4)

Felix Kling
Felix Kling

Reputation: 816272

Typical function-in-a-loop problem. You have to "capture" the current value of x by introducing a new scope (through a function, does not have block scope). You could do:

function loadImage(image) {
    $('<img />')
        .load(function(){
            $('#image_'+image).html(''); //remove loading gif
            $('#image_'+image).append(this);
            $.post('/images/destroy_image',{id:image]});
        })
        .attr('src', '/images/get_image?id='+image);
}

for (var i = 0, l = images.length; i < l; i++) { // it seems `images` is an array 
    loadImage(images[i]);
}

Don't use a for...in loop to iterate over arrays.

Upvotes: 3

aroth
aroth

Reputation: 54796

You're running into this issue due to some semantic discrepancies that exist between the way closures are frequently explained and the way they actually work in most languages. Without getting into that topic, you may have better luck with:

for (var index = 0; index < images.length; index++) {
    var img = new Image();
    $(img)
        .load(load(images[index]))
        .attr('src', '/images/get_image?id='+image);
}

function load(image) {
    return function() {
        $('#image_'+image).html(''); //remove loading gif
        $('#image_'+image).append(this);
        $.post('/images/destroy_image',{id:image});
    };
} 

If you want to get into the topic of why this works where the previous example failed, closures are often explained as creating a snapshot of the state that exists in the current lexical scope. This is a somewhat misleading analogy, because unlike a true snapshot which is immutable, the state inside of the closure continues to live and can be modified by things that happen outside of the closure.

Case in point, when you do something like:

for (x in myArray) {
    setTimeout(function() {
        alert(myArray[x]);
    }, 2000);
}

...there is only a single variable instance referred to by x, and the closure is capturing a reference to x as opposed to a snapshot of the current value of x. Thus when subsequent iterations modify the value of the single instance of x that exists in this scope, the change appears in every closure that was created with a reference to x.

And it works when you do:

for (x in myArray) {
    var item = myArray[x];
    setTimeout(makeClosure(item), 2000);
}

function makeClosure(value) {
    return function() {
        alert(value);
    };
}

...because a different variable reference is created on each iteration (by calling makeClosure()), and once created the value is never modified. Each closure that is created carries a reference to the most recently defined value, and thus appears to "remember" the correct value later on when it executes.

Upvotes: 0

davin
davin

Reputation: 45525

jquery's each() function closes over each value in the array, so you don't end up with a reference to just the last value:

$.each(images, function(i,v) {
    var img = new Image();
    $(img)
        .load(function(){
            $('#image_'+v).html(''); //remove loading gif
            $('#image_'+v).append(this);
            $.post('/images/destroy_image',{id:v});
        })
        .attr('src', '/images/get_image?id='+v);
});

Upvotes: 2

ElonU Webdev
ElonU Webdev

Reputation: 2459

Try using a traditional for loop, so you can have a counter. I think the "x" in your foreach loop is the actual object and not the index value, but you use it like an index elsewhere. Does that fix it?

Upvotes: 0

Related Questions