向jQuery getJSON()成功回调函数传递额外参数

23

我以前从未使用回调函数,因此我可能犯了一个非常愚蠢的错误。我认为我在这里有点理解问题,但不知道如何解决它。

我的代码(有点简化)是:

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现在未定义。

6个回答

50
你需要了解什么是闭包。在JavaScript中,每个变量的作用域都有特定的规则:
- 隐式声明或使用var声明的变量的作用域为最近/当前的函数(包括“箭头函数”),如果不在函数内,则为window或适用于执行上下文的其他全局对象(例如在Node.js中为global)。 - 使用let或const声明的变量(在ES5及以上版本中)的作用域为最近的语句块{ /* 不是对象,而是任何可执行语句的地方 */ }。
如果任何代码可以访问当前作用域或任何父级作用域中的变量,这将在该变量周围创建一个闭包,使变量保持活动状态,并保持任何由该变量引用的对象实例化,以便这些父级或内部函数或块可以继续引用变量并访问其值。
因为原始变量仍然处于活动状态,如果稍后在代码的任何地方更改该变量的值,那么当具有对该变量的闭包的代码稍后运行时,它将具有更新/更改后的值,而不是函数或作用域创建时的值。
现在,在我们解决闭包问题之前,请注意在循环中重复声明没有使用let或const的title变量是不起作用的。var变量被提升到最近函数的作用域中,并且未使用var分配的变量(不引用任何函数作用域)会被隐式附加到全局作用域,而在JavaScript中没有作用域的for循环,因此在其中声明的变量实际上只声明了一次,尽管看起来在循环内(重新)声明。将变量声明放在循环外应该有助于澄清您的代码为什么不能按预期工作。
现在的问题是,当回调运行时,因为它们对相同的变量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 invoking createCallback() returns*, which is a function taking one `data` parameter.
}

注意:由于你的数组显然只有标题,因此你可以考虑使用title变量来替代i,这样就不需要回到some_array了。但是无论哪种方式都可以,你知道自己想要什么。
一种潜在有用的思考方式是,回调函数创建函数(无论是匿名函数还是createCallback函数)本质上将i变量的转换为单独的thisi变量,每次引入具有自己作用域的新函数。也许可以说,“参数将值从闭包中分离出来”。
只需小心:这种技术对于没有复制的对象不起作用,因为对象是引用类型。仅仅将它们作为参数传递并不会产生不可更改的结果。你可以随意复制一个街道地址,但这并不会创建一个新房子。如果你想要通向不同地方的地址,必须建造一个新房子。

6
你可以使用立即执行函数(立即执行的函数)创建闭包,返回另一个函数:
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);
        };
    })());
}

我尝试着去适应这个,但现在data的值从哪里获取?它对我来说现在是未定义的。 - Chris
本来想发帖说原始代码不起作用,但我看到你已经修复了。请注意,jslint会要求您将var声明移动到循环外,并将匿名函数调用放在括号内而不是外部。 - ErikE
Chris - 我已经更新了代码,现在数据是返回函数的一个参数。 - patorjk
太好了,这很有效。现在我有点难以选择正确的答案。你的方法对我很有帮助,但是Erik详细解释了闭包。 - Chris
由于选择答案是由您决定的,但请确保点赞任何对您有帮助的回答。 - patorjk

3
如果您可以修改 some.url 上的服务,那么最好的做法是不要为 some_array 中的每个项目单独发起HTTP请求,而是将数组中的所有项目一次性传递给一个HTTP请求。这样会更加高效。
$.getJSON('some.url', { items: some_array }, callback);

您的数组将被JSON序列化并POST到服务器。假设some_array是一个字符串数组,请求将如下所示:

POST some.url HTTP/1.1
...

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

您的服务器脚本应从请求正文反序列化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请求好一个数量级。)

1
这些是很好的观点。将请求组合起来可能是最好的。 - ErikE

1
创建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));
}

请注意,jslint会要求您将var声明移动到循环外部,并将匿名函数调用放在括号内而不是外部。 - ErikE

0

我认为某些浏览器在同时进行多个异步调用时会出现问题,因此您可以逐个进行调用:

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);

2
不要使用async:false。它会导致整个浏览器,包括UI线程,在网络请求期间被锁定。如果你冻结了用户的浏览器,他们会非常不开心。 - josh3736
Firefox 和 Chrome 只会锁定当前标签页,但意见已被收到,我也从我的回答中移除了该内容。 - Jason Goemaat

0
我遇到了与OP完全相同的问题,但是我用不同的方法解决了它。我用jQuery $.each替换了我的JavaScript 'for'循环,每次迭代都调用一个函数,我认为这可以解决回调“时间”问题。我将我的外部数据数组合并成一个JavaScript对象,以便我可以引用JSON URL上传递的参数和该对象元素中的其他字段。我的对象元素来自使用PHP的mySQL数据库表。
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;
      });
    });
  });
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接