JavaScript:在 For 循环中创建函数

48

最近,我发现自己需要创建一个函数数组。这些函数使用来自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)?

非常感谢您的提前帮助。

3个回答

42
第二种方法会更清晰一些,如果你使用一个不会掩盖循环变量名的参数名称:
funArr[funArr.length] = (function(val) { return function(){  return val; }})(i);

您当前代码的问题在于每个函数都是一个 closure,它们都引用同一个变量 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,请坚持我之前写的内容,或在使用之前将其通过转译器运行最后一遍。)

我是JS的新手,非常感谢你的帖子,让我学会了如何动态地创建函数(在我的情况下是回调函数)! - mineroot
非常有帮助。我刚刚用这个解决了我的问题。http://stackoverflow.com/questions/41254076/preventing-lazy-evaluation-in-function-called-by-fabric-js/41254197#41254197 非常感谢。 - Ari B. Friedman

7

让我们更仔细地研究一下代码并为其分配虚构的函数名称:

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

在这里,outer 接收一个参数 i。JavaScript 使用函数作用域,这意味着每个变量只存在于其定义的函数内部。这里的 i 是在 outer 中定义的,因此存在于 outer(以及任何封闭范围内)。 inner 包含对变量 i 的引用。(请注意,它不会将 i 重新定义为参数或使用 var 关键字!)JavaScript 的作用域规则表明,这样的引用应该与第一个封闭范围相关联,这里是 outer 的作用域。因此,inner 中的 i 引用与 outer 中的相同。
最后,在定义函数 outer 后,我们立即调用它,并传递值 i(这是一个单独的变量,在最外层作用域中定义)。值 i 封闭在 outer 中,其值现在不能被最外层作用域内的任何代码改变。因此,当最外层的 ifor 循环中递增时,outer 中的 i 保持相同的值。
记住,我们实际上创建了许多匿名函数,每个函数都有自己的作用域和参数值,希望现在很清楚这就是为什么每个匿名函数都保留其自己的 i 值的原因。
最后,为了完整起见,让我们来看一下原始代码发生了什么:
for(var i = 0; i < 10; ++i){
    numArr[numArr.length] = i;
    funArr[funArr.length] = function(){  return i; };
}

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


6
我建议您阅读《JavaScript权威指南》这样的书籍,以深入了解JavaScript的基础知识,从而避免像这样常见的陷阱。此外,下面这个答案提供了一个不错的闭包解释:JavaScript闭包是如何工作的? 当您调用函数时,闭包就会产生。
function() { return i; }

实际上,该函数在父调用对象(作用域)中执行变量查找,这就是 i 的定义所在的地方。在本例中,i 被定义为 10,因此每个函数都将返回 10。这样做的原因是

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

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


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