Подтвердить что ты не робот

Передайте дополнительный параметр функции jQuery getJSON()

Мне никогда не приходилось использовать функции обратного вызова, поэтому я, возможно, совершил совершенно тупую ошибку. Я думаю, что я немного понимаю проблему здесь, но не как ее решить.

Мой код (немного упрощен):

for (var i = 0; i < some_array.length; i++) {
    var title = some_array[i];
    $.getJSON('some.url/' + title, function(data) {
        do_something_with_data(data, i);
    }

Теперь, насколько я понимаю, эта анонимная функция будет вызываться только в том случае, если getJSON() получил данные. Но к этому моменту i не имеет значения, которое я бы требовал. Или, насколько мое наблюдение идет, оно имеет последнее значение, которое было бы после завершения цикла (не должно ли оно быть вне пределов?).

В результате, если массив имел размер 6, do_something_with_data() будет вызываться пять раз со значением 5.

Теперь я подумал, просто передайте i анонимной функции

function(data, i) { }

но это не представляется возможным. i теперь undefined.

4b9b3361

Ответ 1

Вам нужно понять, что такое закрытие. В javascript, когда внутри функции вы используете переменную, которая была определена во внешнем контексте (внешняя функция или глобальная), вы создаете замыкание вокруг этой переменной, которое сохраняет эту переменную, и позволяет функции продолжать ссылаться на нее каждый раз, когда она (как и любые другие экземпляры функций с закрытием элемента).

Поскольку исходная переменная все еще создается, если вы изменяете значение этой переменной в любом месте кода, когда функция запускается позже, она будет иметь текущее измененное значение, а не значение, когда функция была впервые создана.

Прежде чем обращать внимание на правильность работы замыкания, обратите внимание, что объявление переменной title в цикле не работает (на самом деле вы можете думать о том, что переменная по существу поднимается в область function в отличие от некоторых других языков, циклы for в JavaScript не имеют области видимости, поэтому переменная объявляется только один раз для функции и не объявляется или не обновляется внутри цикла). Объявление переменной вне цикла должно помочь вам объяснить, почему ваш код не работает так, как вы ожидали.

Как есть, когда обратные вызовы выполняются, потому что они имеют замыкание по той же переменной i, все они затронуты, когда i увеличивается, и все они будут использовать текущее значение i, когда они запускаются (что будет неправильным, поскольку вы обнаружили, потому что обратные вызовы запускаются после того, как цикл полностью завершил создание обратных вызовов). Асинхронный код (например, ответ на вызов JSON) не работает и не может работать до тех пор, пока все синхронные коды не закончат выполнение, поэтому цикл будет завершен до того, как будет выполнен любой обратный вызов.

Чтобы обойти это, вам понадобится новая функция для запуска, которая имеет свою собственную область видимости, поэтому в обратных вызовах, объявленных внутри цикла, происходит новое замыкание каждого значения. Вы можете сделать это с помощью отдельной функции или просто использовать вызываемую анонимную функцию в параметре обратного вызова. Вот пример:

var title, i;
for (i = 0; i < some_array.length; i += 1) {
    title = some_array[i];
    $.getJSON(
       'some.url/' + title,
       (function(thisi) {
          return function(data) {
             do_something_with_data(data, thisi);
             // Break the closure over `i` via the parameter `thisi`,
             // which will hold the correct value from *invocation* time.
          };
       }(i)) // calling the function with the current value
    );
}

Для ясности я разберу его на отдельную функцию, чтобы вы могли видеть, что происходит:

function createCallback(item) {
   return function(data) {
      do_something_with_data(data, item);
      // This reference to the `item` parameter does create a closure on it.
      // However, its scope means that no caller function can change its value.
      // Thus, since we don't change `item` anywhere inside `createCallback`, it
      // will have the value as it was at the time the createCallback function
      // was invoked.
   };
 }

var title, i, l = some_array.length;
for (i = 0; i < l; i += 1) {
    title = some_array[i];
    $.getJSON('some.url/' + title, createCallback(i));
    // Note how this parameter is not a *reference* to the createCallback function,
    // but the *value that createCallback() returns*, which is itself a function.
}

Примечание. Поскольку ваш массив, по-видимому, имеет только заголовки, вы можете использовать вместо переменной i переменную title, которая требует от вас вернуться к some_array. Но в любом случае вы знаете, что хотите.

Один потенциально полезный способ подумать об этом, что функция создания обратного вызова (либо анонимная, либо createCallback) по существу преобразует значение переменной i в отдельные переменные thisi через каждый раз вводя новую функцию с ее собственным объемом. Возможно, можно сказать, что "параметры разрушают значения из замыканий".

Просто будьте осторожны: этот метод не будет работать на объектах без их копирования, поскольку объекты являются ссылочными типами. Простое прохождение их в качестве параметров не даст того, что невозможно изменить после факта. Вы можете дублировать уличный адрес, который вам нравится, но это не создает новый дом. Вы должны построить новый дом, если вам нужен адрес, который приведет к чему-то другому.

Ответ 2

Вы можете создать закрытие, используя немедленную функцию (тот, который выполняется сразу), которая возвращает другую функцию:

for (var i = 0; i < some_array.length; i++) {
    var title = some_array[i];
    $.getJSON('some.url/' + title, (function() {
        var ii = i;
        return function(data) {
           do_something_with_data(data, ii);
        };
    })());
}

Ответ 3

Если вы можете изменить службу в some.url, она будет намного лучше, чем вместо отдельного HTTP-запроса для каждого элемента в some_array, вы просто передаете каждый элемент в массиве в одном HTTP-запросе.

$.getJSON('some.url', { items: some_array }, callback);

Ваш массив будет JSON-сериализован и отправлен на сервер. Предполагая, что some_array - это массив строк, запрос будет выглядеть так:

POST some.url HTTP/1.1
...

{'items':['a','b','c', ... ]}

Затем ваш сервер script должен десериализовать запрос JSON из тела запроса и перебрать все элементы в массиве items, возвращая массив ответов, упорядоченный по JSON.

HTTP/1.1 200 OK
...

{'items':[{id:0, ... }, {id:1, ... }, ... ]}

(Или любые данные, которые вы возвращаете.) Если ваши элементы ответа находятся в том же порядке, что и элементы запроса, легко скомпоновать вещи. В вашем обратном вызове успеха просто сопоставьте индекс элемента с индексом some_array. Объединяя все это:

$.getJSON('some.url', { items: some_array }, function(data) {
    for (var i = 0; i < data.items.length; i++) {
        do_something_with_data(data.items[i], i);
    }
});

Посредством "загрузки" ваших запросов в один HTTP-запрос, подобный этому, вы значительно улучшаете производительность. Учтите, что если каждая сеть в оба конца занимает не менее 200 мс, с 5 пунктами, вы смотрите на минимальную задержку в 1 секунду. Обращаясь к ним все сразу, сетевая задержка остается постоянной 200 мс. (Очевидно, что с большими запросами вступает в действие исполнение сервера и серверное время и время передачи сети, но производительность по-прежнему будет на порядок лучше, чем если вы выдаете отдельный HTTP-запрос для каждого элемента.)

Ответ 4

Создайте N замыканий и передайте значение "i" каждый раз, например:

var i, title;
for (i = 0; i < some_array.length; i++) {
    title = some_array[i];
    $.getJSON('some.url/' + title, (function(i_copy) {
        return function(data) {
            do_something_with_data(data, i_copy);
        };
    })(i));
}

Ответ 5

Я думаю, что у некоторых браузеров есть проблемы с одновременным выполнением нескольких асинхронных вызовов, поэтому вы можете сделать их по одному:

var i;
function DoOne(data)
{
    if (i >= 0)
        do_something_with_data(data, i);
    if (++i >= some_array.length)
        return;
    var title = some_array[i];
    $.getJSON('some.url/' + title, DoOne);
}

// to start the chain:
i = -1;
DoOne(null);

Ответ 6

У меня была точно такая же проблема, как и у OP, но она была решена по-другому. Я заменил цикл JavaScript для цикла jQuery $.each, который для каждой итерации вызывает функцию, которая, как мне кажется, преодолевает проблему обратного вызова. И я объединил свои внешние массивы данных в объект JavaScript, чтобы я мог ссылаться как на параметр, который я передавал по URL JSON, так и на другое поле в том же элементе этого объекта. Элементы моего объекта вышли из таблицы базы данных mySQL с помощью PHP.

var persons = [
 { Location: 'MK6', Bio: 'System administrator' },
 { Location: 'LU4', Bio: 'Project officer' },
 { Location: 'B37', Bio: 'Renewable energy hardware installer' },
 { Location: 'S23', Bio: 'Associate lecturer and first hardware triallist' },
 { Location: 'EH12', Bio: 'Associate lecturer with a solar PV installation' }
];

function initMap() {
  var map = new google.maps.Map(document.getElementById('map_canvas'), {
    center: startLatLon,
    minZoom: 5,
    maxZoom: 11,
    zoom: 5
  });
  $.each(persons, function(x, person) {
    $.getJSON('http://maps.googleapis.com/maps/api/geocode/json?address=' + person.Location, null, function (data) {
      var p = data.results[0].geometry.location;
      var latlng = new google.maps.LatLng(p.lat, p.lng);
      var image = 'images/solarenergy.png';
      var marker = new google.maps.Marker({
        position: latlng,
        map: map,
        icon: image,
        title: person.Bio
      });
      google.maps.event.addListener(marker, "click", function (e) {
        document.getElementById('info').value = person.Bio;
      });
    });
  });
}