46

最近,我发现自己需要创建一个函数数组。这些函数使用 XML 文档中的值,并且我正在使用 for 循环通过适当的节点运行。然而,在这样做之后,我发现数组中的所有函数只使用了 XML 表的最后一个节点(对应于 for 循环的最后一次运行)。

以下是展示这一点的示例:

var numArr = [];
var funArr = [];
for(var i = 0; i < 10; ++i){
    numArr[numArr.length] = i;
    funArr[funArr.length] = function(){  return i; };
}

window.alert("Num: " + numArr[5] + "\nFun: " + funArr[5]());

输出为 Num:5 和 Fun:10。

经过研究,我发现了一段有效的代码,但我很难准确理解它为什么有效。我在这里使用我的示例复制了它:

var funArr2 = [];
for(var i = 0; i < 10; ++i)
    funArr2[funArr2.length] = (function(i){ return function(){ return i;}})(i);

window.alert("Fun 2: " + funArr2[5]());

我知道它与范围界定有关,但乍一看,它的表现似乎与我幼稚的方法没有任何不同。我有点 Javascript 的初学者,所以如果我可能会问,为什么使用这种函数返回函数技术绕过范围问题?另外,为什么最后会包含(i)?

非常感谢您提前。

4

3 回答 3

42

如果您使用不屏蔽循环变量名称的参数名称,则第二种方法会更清楚一些:

funArr[funArr.length] = (function(val) { return function(){  return val; }})(i);

您当前代码的问题是每个函数都是一个闭包,它们都引用同一个变量i。当每个函数运行时,它返回函数运行时的值i(这将比循环的限制值大一)。

更清晰的方法是编写一个单独的函数来返回您想要的闭包:

var numArr = [];
var funArr = [];
for(var i = 0; i < 10; ++i){
    numArr[numArr.length] = i;
    funArr[funArr.length] = getFun(i);
}

function getFun(val) {
    return function() { return val; };
}

请注意,这与我的答案中的第一行代码基本相同:调用返回函数的函数并将值i作为参数传递。它的主要优点是清晰度。

编辑:现在几乎所有地方都支持 EcmaScript 6(对不起,IE 用户),您可以使用更简单的方法来解决问题——使用let关键字而不是var循环变量:

var numArr = [];
var funArr = [];
for(let i = 0; i < 10; ++i){
    numArr[numArr.length] = i;
    funArr[funArr.length] = function(){  return i; };
}

有了这个微小的变化,每个funArr元素都是一个闭包绑定,在每次循环迭代中执行不同的对象。 i有关更多信息let,请参阅2015 年的这篇 Mozilla Hacks 帖子。(如果您的目标环境不支持let.

于 2013-10-30T23:54:16.393 回答
7

让我们更深入地研究一下代码的作用并分配虚构的函数名称:

(function outer(i) { 
    return function inner() { 
        return i;
    }
 })(i);

在这里,outer接收一个参数i。JavaScript 使用函数作用域,这意味着每个变量只存在于定义它的函数中。i这里定义在 中outer,因此存在于outer(以及包含在其中的任何作用域)中。

inner包含对变量的引用i。(请注意,它不会重新定义i为参数或var关键字!)JavaScript 的作用域规则规定,此类引用应绑定到第一个封闭作用域,这里是outer. 因此,iwithininner指的是与 inside 相同i的内容outer

最后,在定义函数之后outer,我们立即调用它,将值传递给它i(这是一个单独的变量,定义在最外层范围内)。该值i包含在 内outer,并且它的值现在不能被最外层范围内的任何代码更改。因此,当最外层在循环i中递增时,内部保持相同的值。foriouter

记住我们实际上已经创建了许多匿名函数,每个函数都有自己的作用域和参数值,希望清楚这些匿名函数中的每一个如何为i.

最后,为了完整起见,让我们检查一下原始代码发生了什么:

for(var i = 0; i < 10; ++i){
    numArr[numArr.length] = i;
    funArr[funArr.length] = function(){  return i; };
}

在这里,我们可以看到匿名函数包含对最外层的引用i。随着该值的变化,它将反映在匿名函数中,该函数不会以任何形式保留其自己的值副本。因此,由于i == 10在我们去调用我们创建的所有这些函数时,在最外层范围内,每个函数都将返回 value 10

于 2013-10-31T00:00:17.653 回答
6

我建议阅读像JavaScript: The Definitive Guide这样的书,以更深入地了解 JavaScript,这样你就可以避免像这样的常见陷阱。

这个答案还专门针对闭包提供了一个不错的解释:

JavaScript 闭包是如何工作的?

当你调用

function() { return i; }

该函数实际上是在父调用对象(范围)上进行变量查找,这是定义 i 的地方。在这种情况下,i 被定义为 10,因此这些函数中的每一个都将返回 10。这是有效的原因

(function(i){ return function(){ return i;}})(i);

是通过立即调用匿名函数,创建了一个新的调用对象,其中定义了当前 i。因此,当您调用嵌套函数时,该函数指的是匿名函数的调用对象(它定义了您在调用它时传递给它的任何值),而不是最初定义 i 的范围(仍然是 10) .

于 2013-10-31T01:26:29.620 回答