JavaScript回调函数和递归

17

这是一道类似智力题的问题,因为代码本身完全可以正常工作,只是让我感觉稍微有点不爽。我向Stack Overflow求助是因为我自己的大脑暂时失灵了。

以下是使用Google Maps JS API查找地址并在地图上放置标记的代码片段。但是,有时初始查找会失败,因此我想用另一个地址重复该过程。

geocoder.getLatLng(item.mapstring, function(point) {
    if (!point) {
        geocoder.getLatLng(item.backup_mapstring, function(point) {
            if (!point) return;
            map.setCenter(point, 13);
            map.setZoom(7);
            map.addOverlay(new GMarker(point));
        })
        return;
    }
    map.setCenter(point, 13);
    map.setZoom(7);
    map.addOverlay(new GMarker(point));
})

(getLatLng 的第二个参数是一个回调函数。)

当然,你可以看到在主要回调和“备用回调”中重复了三行代码,这些代码居中、缩放地图并添加标记。你能找到一种不重复的表达方式吗?如果你的解决方案适用于任意数量的备用地图字符串,你将获得额外的奖励分数和我的赞赏。

4个回答

21

其他答案都不错,但这里还有一种选择。这种方法允许您保留与开始时相同的形式,但使用命名lambda函数的技巧,以便可以递归地引用它:

mapstrings = ['mapstring1', 'mapstring2', 'mapstring3'];

geocoder.getLatLng(mapstrings.shift(), function lambda(point) {
   if(point) {
        // success
        map.setCenter(point, 13);
        map.setZoom(7);
        map.addOverlay(new GMarker(point));
    }
    else if(mapstrings.length > 0) {
        // Previous mapstring failed... try next mapstring
        geocoder.getLatLng(mapstrings.shift(), lambda);
    }
    else {
        // Take special action if no mapstring succeeds?
    }
})

第一次使用符号"lambda"是为了将其引入为新的函数字面名称。第二次使用它是一个递归引用。

在Chrome中,函数字面命名工作正常,我假设它在大多数现代浏览器中也能正常工作,但我没有测试过并且对于旧版浏览器我不了解。


4
直译的命名方式比你的解决方案更加简洁明了,而且不会产生太多的交互。 - 1800 INFORMATION
1
如果你看着一段代码,发现自己捏着鼻子,那很可能是一个好的迹象。 - 1800 INFORMATION
2
那个arguments.callee的东西对于没有很多JavaScript经验的人来说会非常不明显。这肯定更易于维护。 - Jay Kominek
1
命名的 lambda 表达式让我想到:“我认为所有的编程语言都应该有这个”。而 arguments.callee 这个东西则让我想到:“多么巧妙的 hack,我可以想象在某些地方需要用到它”。 - 1800 INFORMATION
你确实需要字面命名,arguments.callee会破坏严格模式。引用自MDN:避免使用arguments.callee(),可以给函数表达式命名或者在必须调用自身的函数中使用函数声明。https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/arguments/callee - Lukas Liesis
显示剩余2条评论

8
在不显式支持递归的语言结构中,有一种非常好的递归方法,称为“不动点组合子”。最著名的是Y-Combinator
以下是JavaScript中一个参数函数的Y combinator
function Y(le, a) {
    return function (f) {
        return f(f);
    }(function (f) {
        return le(function (x) {
            return f(f)(x);
        }, a);
    });
}

这看起来有点可怕,但你只需要写一次。使用它实际上非常简单。基本上,您需要将原始的一个参数的 lambda 转换为一个新的两个参数的函数 - 第一个参数现在是实际的 lambda 表达式,您可以在其上进行递归调用,第二个参数是您想要使用的原始第一个参数(point)。
以下是如何在示例中使用它。请注意,我正在使用 mapstrings 作为要查找的字符串列表,而 pop 函数会从头部破坏性地删除一个元素。
geocoder.getLatLng(pop(mapstrings), Y(
  function(getLatLongCallback, point)
  {
    if (!point)
    {
      if (length(mapstrings) > 0)
        geocoder.getLatLng(pop(mapstrings), getLatLongCallback);
      return;
    }

    map.setCenter(point, 13);
    map.setZoom(7);
    map.addOverlay(new GMarker(point));
  });

2

是的,将其提取到一个函数中 :)

geocoder.getLatLng(item.mapstring, function(point) {
    if (!point) {
        geocoder.getLatLng(item.backup_mapstring, function(point) {
                if (point) {
                    setPoint(point);
                }
        })
        return;
    }

    function setPoint(point) {
        map.setCenter(point, 13);
        map.setZoom(7);
        map.addOverlay(new GMarker(point));
    }

    setPoint(point);
});

1

这个怎么样?

function place_point(mapstrings,idx)
{
    if(idx>=mapstrings.length) return;
    geocoder.getLatLng(mapstrings[idx],
                       function(point)
                       {
                           if(!point)
                           {
                               place_point(mapstrings,idx+1);
                               return;
                           }
                           map.setCenter(point, 13);
                           map.setZoom(7);
                           map.addOverlay(new GMarker(point));
                       });
}

你可以使用任意数量的备用字符串。第一次调用时,请将第二个参数传入0。


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