22

I'm new to using AJAX, and I'm writing a userscript that will process a bunch of links on a page and make AJAX calls for each one.

for (var i = 0; i < linkList.length; i++)
{
    $.ajax({
        url: linkList[i].getAttribute("href"),
        cache: false
    }).done(function( html )
    {
        var hasAppended = false;
        if (html.indexOf('someStringOnGottenPage') != -1 && !hasAppended)
        {
            hasAppended = true;
            var id = linkList[i].getAttribute("href").substring(linkList[i].getAttribute("href").indexOf('='));
            $( "#links a[href*='" + id + "']" ).append(' THIS PAGE CONTAINS SPECIFIED DATA');
        }
    });
}

To try to put it simply, I have a page with a list of links. I wish to iterate through the links and get AJAX to process the contents of each of the links pages, and report back if that page contains something specified.

The issue I'm having is the value of [i] used to iterate through the linkList is always 6, and it should never be. I'm assuming I need to pass some data so that when .done finally triggers, it knows its [i] value from when AJAX first triggered and not the value of [i] when .done triggers later.

How do I go about ensuring .done knows it's [i] value when AJAX is first called?

Edge
  • 2,456
  • 6
  • 32
  • 57

2 Answers2

45

The easiest way is to use a closure. Whenever you have something asynchronous in a loop, it is the same thing.

for (var i .....) {
  asynchronousFunction(function() {
    use(i);
  }
}

In this pseudocode snippet, the inner function captures the storage location referenced by i. The loop runs, the i increments to its final value, and then the async callbacks start getting called, all of them looking up the exact same location (not value).

The general solution is this:

for (var i .....) {
  (function (i) {
    asynchronousFunction(function() {
      use(i);
    });
  })(i);
}

i.e. wrap the whole contents of your loop in an self-executing function.

Here, the value of outer i gets passed into the wrapping self-executing anonymous function; this unique value's location gets captured by the async callback. In this way, each async gets its own value, determined at the moment the self-executing function is invoked.

Amadan
  • 191,408
  • 23
  • 240
  • 301
  • 4
    Adding the (function (i) { CODE })(i); wrapper works perfectly, now the code runs with the intended result. Thankyou! – Edge Feb 17 '14 at 03:03
  • 7
    So THAT's the point of a closure – LazerSharks Jul 17 '14 at 03:31
  • 1
    you saved my life – krummens May 02 '17 at 22:11
  • It throws: "async its not defined" – Pedro Joaquín Nov 11 '19 at 20:03
  • @PedroJoaquin: For one thing, the answer was written before `async` was a keyword. For another, this is a pseudocode, and `async` was a metavariable here, standing for any asynchronous function that accepts a callback, just like `use()` is a metavariable for any task that should be done inside a callback. I will rename `async` to make it compatible with ES6, but you still have to replace with function you are actually using (such as `$.ajax` in the OP). – Amadan Nov 12 '19 at 03:23
20

The links in the comment section of the question tells you what is wrong in your code... but you can have a better solution than the once explained there.

Try $.each() to iterate through the list(assuming it is an array), so that the callback passed will create a separate closure for each iteration

$.each(linkList, function (i, item) {
    $.ajax({
        url: item.getAttribute("href"),
        cache: false
    }).done(function (html) {
        var hasAppended = false;
        if (html.indexOf('someStringOnGottenPage') != -1 && !hasAppended) {
            hasAppended = true;
            var id = item.getAttribute("href").substring(item.getAttribute("href").indexOf('='));
            $("#links a[href*='" + id + "']").append(' THIS PAGE CONTAINS SPECIFIED DATA');
        }
    });
})

If it is an jQuery object then use .each()

linkList.each(function (i, item) {
    var $item = $(item),
        href = $item.attr("href");
    $.ajax({
        url: href,
        cache: false
    }).done(function (html) {
        var hasAppended = false;
        if (html.indexOf('someStringOnGottenPage') != -1 && !hasAppended) {
            hasAppended = true;
            var id = href.substring(href.indexOf('='));
            $("#links a[href*='" + id + "']").append(' THIS PAGE CONTAINS SPECIFIED DATA');
        }
    });
})
Arun P Johny
  • 384,651
  • 66
  • 527
  • 531