var funcs = [];
for (var i = 0; i < 3; i++) { // let's create 3 functions
funcs[i] = function() { // and store them in funcs
console.log("My value: " + i); // each should log its value.
};
}
for (var j = 0; j < 3; j++) {
funcs[j](); // and now let's run each one to see
}
它输出这个:
我的 Value :3我的 Value :3我的 Value :3
而我希望它输出:
我的 Value :0我的 Value :1我的 Value :2
使用事件侦听器导致运行函数的延迟时,会出现同样的问题:
var buttons = document.getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) { // let's create 3 functions
buttons[i].addEventListener("click", function() { // as event listeners
console.log("My value: " + i); // each should log its value.
});
}
<button>0</button><br>
<button>1</button><br>
<button>2</button>
......或异步代码,例如使用承诺:
// Some async wait function
const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));
for(var i = 0; i < 3; i++){
wait(i * 100).then(() => console.log(i)); // Log `i` as soon as each promise resolves.
}
这个基本问题的解决方案是什么?
30 回答
这是一个使用
forEach
的简单解决方案(回到IE9):打印:
好吧,问题是每个匿名函数中的变量
i
都绑定到函数外部的同一个变量 .经典解决方案:闭包
你想要做的是将每个函数中的变量绑定到函数之外的一个单独的,不变的值:
由于JavaScript中没有块作用域 - 只有函数作用域 - 通过将函数创建包装在新函数中,可以确保“i”的值保持不变 .
2015解决方案:forEach
由于
Array.prototype.forEach
函数的相对广泛的可用性(在2015年),值得注意的是,在主要涉及值数组的迭代中,.forEach()
提供了一种干净,自然的方式来为每次迭代获得明显的闭包 . 也就是说,假设您有某种包含值的数组(DOM引用,对象等等),并且设置了特定于每个元素的回调问题,您可以这样做:我们的想法是,与
.forEach
循环一起使用的回调函数的每次调用都将是它自己的闭包 . 传递给该处理程序的参数是特定于该迭代的特定步骤的数组元素 . 如果它与在迭代的其他步骤中 Build 的任何其他回调冲突 .如果你碰巧在jQuery中工作,那么
$.each()
函数会为你提供类似的功能 .ES6解决方案:让
ECMAScript 6(ES6)引入了新的
let
和const
关键字,其范围与基于var
的变量不同 . 例如,在具有基于let
的索引的循环中,循环中的每次迭代都将具有i
的新值,其中每个值都限定在循环内,因此您的代码将按预期工作 . 有很多资源,但我建议2ality's block-scoping post作为一个很好的信息来源 .但要注意,IE9-IE11和Edge 14之前的Edge支持
let
但是上面的错误(他们每次都没有创建新的i
,所以上面的所有函数都会记录3,就像我们使用var
一样) . Edge 14最终做对了 .尝试:
Edit (2014):
我个人认为@ Aust的more recent answer about using .bind是现在做这种事情的最佳方式 . 有's also lo-dash/underscore' s
_.partial
当你不需要或想要弄乱bind
的thisArg
时 .另一种尚未提及的方法是使用Function.prototype.bind
UPDATE
正如@squint和@mekdev所指出的那样,通过首先在循环外创建函数然后在循环中绑定结果,可以获得更好的性能 .
使用Immediately-Invoked Function Expression,最简单,最易读的方法来封装索引变量:
这将迭代器
i
发送到我们定义为index
的匿名函数中 . 这将创建一个闭包,其中保存变量i
以供稍后在IIFE中的任何异步功能中使用 .派对迟到了,但我今天正在探讨这个问题,并注意到许多答案并没有完全解决Javascript如何处理范围,这基本上归结为这个问题 .
正如许多其他人提到的那样,问题是内部函数引用了相同的
i
变量 . 那么为什么我们不在每次迭代时只创建一个新的局部变量,而是使用内部函数引用呢?就像之前一样,每个内部函数输出分配给
i
的最后一个值,现在每个内部函数只输出分配给ilocal
的最后一个值 . 但是't each iteration have it'不应该ilocal
?事实证明,这就是问题所在 . 每次迭代都共享相同的范围,因此第一次迭代后的每次迭代都只是覆盖
ilocal
. 来自MDN:重申强调:
我们可以通过在每次迭代中声明它之前检查
ilocal
来看到这一点:这正是这个bug如此棘手的原因 . 即使您重新声明变量,Javascript也不会抛出错误,JSLint甚至不会发出警告 . 这也是为什么解决这个问题的最好方法是利用闭包,这本质上是在Javascript中,内部函数可以访问外部变量,因为内部作用域“包围”外部作用域 .
这也意味着内部函数"hold onto"外部变量并保持它们活着,即使外部函数返回 . 为了利用这个,我们创建并调用一个包装器函数纯粹是为了创建一个新的作用域,在新作用域中声明
ilocal
,并返回一个使用ilocal
的内部函数(下面有更多解释):在包装函数中创建内部函数为内部函数提供了一个只有它才能访问的私有环境"closure" . 因此,每次调用包装器函数时,我们都会使用它自己独立的环境创建一个新的内部函数,确保
ilocal
变量不会相互碰撞和覆盖 . 一些小的优化给出了许多其他SO用户给出的最终答案:Update
现在ES6已成为主流,我们现在可以使用新的
let
关键字来创建块范围的变量:看看它现在多么容易!有关详细信息,请参阅this answer,我的信息基于 .
OP显示的代码的主要问题是
i
在第二个循环之前永远不会被读取 . 为了演示,想象一下在代码中看到错误在
funcs[someIndex]
执行()
之前,实际上不会发生错误 . 使用相同的逻辑,很明显,在此之前也不会收集i
的值 . 一旦原始循环结束,i++
将i
带到3
的值,这导致条件i < 3
失败并且循环结束 . 此时,i
是3
,所以当使用funcs[someIndex]()
,并且i
被评估时,它每次都是3 .为了解决这个问题,您必须在遇到问题时评估
i
. 请注意,这已经以funcs[i]
(其中有3个唯一索引)的形式发生 . 有几种方法可以捕获此值 . 一种是将其作为参数传递给函数,该函数已经以几种方式显示在此处 .另一个选择是构造一个能够关闭变量的函数对象 . 这可以这样完成
jsFiddle Demo
随着ES6现在得到广泛支持,这个问题的最佳答案已经改变 . ES6为此确切情况提供了
let
和const
个关键字 . 我们可以使用let
来设置一个循环范围变量,而不是乱搞闭包:然后
val
将指向特定于循环的特定转弯的对象,并且将返回正确的值而不使用附加的闭包符号 . 这显然简化了这个问题 .const
类似于let
,其附加限制是变量名称在初始赋值后无法回弹到新引用 .浏览器支持现在适用于针对最新版浏览器的用户 .
const
/let
目前支持最新的Firefox,Safari,Edge和Chrome . Node也支持它,你可以利用像Babel这样的构建工具在任何地方使用它 . 你可以在这里看到一个有效的例子:http://jsfiddle.net/ben336/rbU4t/2/文件在这里:
const
let
但要注意,IE9-IE11和Edge 14之前的Edge支持
let
但是得到了上述错误(它们每次都没有创建新的i
,所以上面的所有函数都会记录3,就像我们使用var
一样) . Edge 14最终做对了 .另一种说法是函数中的
i
在执行函数时被绑定,而不是创建函数的时间 .创建闭包时,
i
是对外部作用域中定义的变量的引用,而不是创建闭包时的副本 . 它将在执行时进行评估 .大多数其他答案提供了通过创建另一个不会为您更改值的变量来解决的方法 .
我想我会添加一个清晰的解释 . 对于一个解决方案,就个人而言,我会选择Harto,因为从这里的答案来看,这是最不言自明的方式 . 发布的任何代码都可以使用,但我选择封闭工厂而不必写一堆注释来解释为什么我要声明一个新变量(Freddy和1800's)或者有奇怪的嵌入式闭包语法(apphacker) .
你需要了解的是javascript中变量的范围是基于函数的 . 这是一个重要的区别,而不是c#,你有块范围,只是将变量复制到for内的一个将起作用 .
将它包装在一个函数中,将函数评估为像apphacker的答案一样返回函数,这样做就可以了,因为变量现在具有函数范围 .
还有一个let关键字而不是var,允许使用块范围规则 . 在那种情况下,在for中定义变量就可以了 . 也就是说,由于兼容性,let关键字不是一个实用的解决方案 .
这是该技术的另一种变体,类似于Bjorn(apphacker),它允许您在函数内部分配变量值,而不是将其作为参数传递,有时可能更清晰:
请注意,无论使用何种技术,
index
变量都会变成一种静态变量,绑定到内部函数的返回副本 . 即,在调用之间保留对其值的更改 . 它可以非常方便 .这描述了在JavaScript中使用闭包的常见错误 .
一个函数定义一个新环境
考虑:
每次调用
makeCounter
时,{counter: 0}
都会导致创建一个新对象 . 此外,还会创建obj
的新副本以引用新对象 . 因此,counter1
和counter2
彼此独立 .循环中的闭包
在循环中使用闭包很棘手 .
考虑:
请注意
counters[0]
和counters[1]
不是独立的 . 事实上,他们的运作方式相同obj
!这是因为在循环的所有迭代中只有一个
obj
的副本,可能是出于性能原因 . 即使{counter: 0}
在每次迭代中创建一个新对象,obj
的相同副本也会通过对最新对象的引用进行更新 .解决方案是使用另一个辅助函数:
这是有效的,因为函数作用域中的局部变量以及函数参数变量在进入时都会分配新的副本 .
有关详细讨论,请参阅JavaScript closure pitfalls and usage
最简单的解决方案是,
而不是使用:
警告"2",共3次 . 这是因为在for循环中创建的匿名函数共享相同的闭包,并且在该闭包中,
i
的值是相同的 . 使用它来防止共享关闭:这背后的想法是,使用IIFE(立即调用的函数表达式)封装for循环的整个主体,并将
new_i
作为参数传递并将其捕获为i
. 由于匿名函数是立即执行的,因此匿名函数内定义的每个函数的i
值都不同 .这个解决方案似乎适合任何这样的问题,因为它需要对遇到此问题的原始代码进行最小的更改 . 事实上,这是设计,它应该不是一个问题!
试试这个较短的一个
没有数组
没有额外的循环
http://jsfiddle.net/7P6EN/
JavaScript函数“关闭”它们在声明时可以访问的范围,并保留对该范围的访问权限,即使该范围中的变量发生更改 .
上面数组中的每个函数都关闭全局范围(全局,只是因为它恰好是它们声明的范围) .
稍后,将调用这些函数,在全局范围内记录
i
的最新值 . 那是魔术,和挫折,关闭 ."JavaScript Functions close over the scope they are declared in, and retain access to that scope even as variable values inside of that scope change."
使用
let
而不是var
通过每次运行for
循环时创建一个新范围来解决此问题,为每个要关闭的函数创建一个单独的范围 . 各种其他技术通过额外功能执行相同的操作 .(
let
使变量块作用域 . 块用花括号表示,但在for循环的情况下,初始化变量,在我们的例子中,i
被认为是在大括号中声明 . )在阅读了各种解决方案之后,我想补充一点,这些解决方案的工作原理是依赖于 scope chain 的概念 . 这是JavaScript在执行期间解析变量的方式 .
每个函数定义形成一个范围,该范围由
var
及其arguments
声明的所有局部变量组成 .如果我们在另一个(外部)函数中定义了内部函数,则会形成一个链,并将在执行期间使用
执行函数时,运行时通过搜索 scope chain 来评估变量 . 如果可以在链的某个点找到变量,它将停止搜索并使用它,否则它将一直持续到达到属于
window
的全局范围 .在初始代码中:
执行
funcs
时,范围链将为function inner -> global
. 由于在function inner
中找不到变量i
(既未使用var
声明也未作为参数传递),它继续搜索,直到i
的值最终在全局范围内找到window.i
.通过将它包装在外部函数中,可以显式定义辅助函数,如harto,或使用像Bjorn这样的匿名函数:
当
funcs
执行时,现在范围链将是function inner -> function outer
. 这个时间i
可以在外部函数的作用域中找到,它在for循环中执行3次,每次都正确绑定值i
. 内部执行时不会使用window.i
的值 .更多细节可以找到here
它包括在循环中创建闭包的常见错误,就像我们在这里所做的那样,以及为什么我们需要闭包和性能考虑 .
通过ES6的新功能,可以管理块级别范围:
OP问题中的代码替换为 let 而不是 var .
我很惊讶没有人建议使用
forEach
函数来更好地避免(重新)使用局部变量 . 事实上,由于这个原因,我根本不再使用for(var i ...)
.//编辑使用
forEach
而不是map .这个问题真的展示了JavaScript的历史!现在我们可以避免使用箭头函数进行块作用域,并使用Object方法直接从DOM节点处理循环 .
首先,了解这段代码的错误:
这里正在初始化
funcs[]
数组时,i
正在递增,funcs
数组被初始化,func
数组的大小变为3,所以i = 3,
. 现在,当funcs[j]()
被调用时,它再次使用变量i
,它已经增加到3 .现在要解决这个问题,我们有很多选择 . 以下是其中两个:
let
初始化i
或用let
初始化一个新变量index
并使其等于i
. 因此,在进行调用时,将使用index
,其范围将在初始化后结束 . 对于呼叫,index
将再次初始化:tempFunc
,返回实际函数:案例1:使用var
现在按 F12 打开您的 chrome console window 并刷新页面 . 在数组中扩展每3个函数 . 您将看到一个名为
[[Scopes]]
.Expand的属性 . 您将看到一个名为"Global"
的数组对象,展开该对象 . 您将在对象中声明属性'i'
,其值为3 .Conclusion:
当您在函数外使用
'var'
声明变量时,它将变为全局变量(您可以通过在控制台窗口中键入i
或window.i
进行检查 . 它将返回3) .您声明的不良函数不会调用并检查该值除非你调用函数,否则在函数内部 .
调用该函数时,
console.log("My value: " + i)
从其Global
对象中获取值并显示结果 .CASE2:使用let
现在用
'let'
替换'var'
做同样的事情,转到范围 . 现在您将看到两个对象
"Block"
和"Global"
. 现在展开Block
对象,你会看到'i'在那里定义了,奇怪的是,对于每个函数,如果i
的值不同(0,1,2) .Conclusion:
当您使用
'let'
甚至在函数外但在循环内声明变量时,此变量将不是全局变量,它将变为Block
级变量,仅适用于同一函数 . 这就是我们获得 Value 的原因当我们调用函数时,每个函数的i
都不同 .有关近距离工作的更多细节,请浏览精彩的视频教程https://youtu.be/71AtaJpJHw0
原始示例不起作用的原因是您在循环中创建的所有闭包都引用了相同的帧 . 实际上,在一个对象上只有一个
i
变量有3个方法 . 他们都打印出相同的 Value .使用closure结构,这将减少你的额外for循环 . 你可以在一个for循环中完成它:
我更喜欢使用
forEach
函数,它有自己的闭包创建一个伪范围:这看起来比其他语言的范围更丑,但恕我直言比其他解决方案更怪异 .
您可以将声明模块用于数据列表,例如query-js(*) . 在这些情况下,我个人认为声明式方法不那么令人惊讶
然后,您可以使用第二个循环并获得预期结果,或者您可以这样做
(*)我是query-js的作者,因此偏向于使用它,所以不要仅仅因为声明性方法而把我的话作为所述库的推荐:)
许多解决方案似乎都是正确的,但它们不会被称为Currying,这是一种功能性编程设计模式,适用于此类情况 . 比绑定快3-10倍,具体取决于浏览器 .
见the performance gain in different browsers .
您的代码不起作用,因为它的作用是:
现在的问题是,调用函数时变量
i
的值是多少?因为第一个循环是在i < 3
的条件下创建的,所以当条件为false时它会立即停止,因此它是i = 3
.您需要了解的是,在创建函数时,没有执行任何代码,只会保存以供日后使用 . 因此,当稍后调用它们时,解释器会执行它们并询问:“
i
的当前值是多少?”所以,你的目标是首先将
i
的值保存到函数中,然后才将函数保存到funcs
. 这可以通过以下方式完成:这样,每个函数都有自己的变量
x
,我们在每次迭代中将x
设置为i
的值 .这只是解决此问题的多种方法之一 .
还有另一种解决方案:只需将
this
绑定到返回函数,而不是创建另一个循环 .通过绑定 this ,也解决了这个问题 .
COUNTER BEING A PRIMITIVE
让我们定义回调函数如下:
超时完成后,将为两者打印2 . 这是因为回调函数基于lexical scope访问该值,其中定义了函数 .
要在定义回调时传递和保留值,我们可以创建一个closure,以在调用回调之前保留该值 . 这可以按如下方式完成:
现在有什么特别之处是“原语是通过值传递并复制的 . 因此,当定义闭包时,它们会保留前一循环的值 . ”
COUNTER BEING AN OBJECT
由于闭包可以通过引用访问父函数变量,因此这种方法与基元的方法不同 .
因此,即使为作为对象传递的变量创建了闭包,也不会保留循环索引的值 . 这是为了表明不会复制对象的值,而是通过引用访问它们 .