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
}
起源
答案:
好吧,问题是变量 i
在每个匿名函数中,都绑定到函数外部的同一个变量。
经典解决方案:闭包
你想要做的是将每个函数中的变量绑定到函数之外的一个单独的,不变的值:
var funcs = [];
function createfunc(i) {
return function() { console.log("My value: " + i); };
}
for (var i = 0; i < 3; i++) {
funcs[i] = createfunc(i);
}
for (var j = 0; j < 3; j++) {
funcs[j](); // and now let's run each one to see
}
由于JavaScript中没有块作用域 - 只有函数作用域 - 通过将函数创建包装在新函数中,可以确保“i”的值保持不变。
2015解决方案:forEach
随着相对广泛的可用性 Array.prototype.forEach
函数(在2015年),值得注意的是,在主要涉及一系列值的迭代中, .forEach()
提供了一种干净,自然的方式来为每次迭代获得明显的闭包。也就是说,假设你有某种类型的数组包含值(DOM引用,对象等等),并且设置了特定于每个元素的回调问题,你可以这样做:
var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
// ... code code code for this one element
someAsynchronousFunction(arrayElement, function() {
arrayElement.doSomething();
});
});
这个想法是每次调用回调函数时使用的 .forEach
循环将是它自己的闭包。传递给该处理程序的参数是特定于该迭代的特定步骤的数组元素。如果它在异步回调中使用,它将不会与在迭代的其他步骤中建立的任何其他回调冲突。
如果你碰巧在jQuery中工作,那么 $.each()
功能为您提供类似的功能。
ES6解决方案: let
ECMAScript 6(ES6)是最新版本的JavaScript,现在开始在许多常绿浏览器和后端系统中实现。也有像 巴别塔 这会将ES6转换为ES5,以允许在旧系统上使用新功能。
ES6引入了新的 let
和 const
范围与...不同的关键字 var
基于变量。例如,在带有a的循环中 let
基于索引,每次迭代循环都会有一个新值 i
其中每个值都在循环内部,因此您的代码将按预期工作。有很多资源,但我建议 2ality的区块范围 作为一个伟大的信息来源。
for (let i = 0; i < 3; i++) {
funcs[i] = function() {
console.log("My value: " + i);
};
}
但请注意,在Edge 14支持之前IE9-IE11和Edge let
但得到上述错误(他们没有创造新的 i
每一次,所以上面的所有函数都会像我们使用的那样记录3 var
)。 Edge 14最终做对了。
尝试:
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = (function(index) {
return function() {
console.log("My value: " + index);
};
}(i));
}
for (var j = 0; j < 3; j++) {
funcs[j]();
}
编辑 (2014):
我个人认为@Aust's 关于使用的最新答案 .bind
是现在做这种事情的最好方法。还有lo-dash / underscore _.partial
当你不需要或想要惹麻烦时 bind
的 thisArg
。
另一种尚未提及的方法是使用 Function.prototype.bind
var funcs = {};
for (var i = 0; i < 3; i++) {
funcs[i] = function(x) {
console.log('My value: ' + x);
}.bind(this, i);
}
for (var j = 0; j < 3; j++) {
funcs[j]();
}
UPDATE
正如@squint和@mekdev所指出的那样,通过首先在循环外创建函数然后在循环中绑定结果,可以获得更好的性能。
function log(x) {
console.log('My value: ' + x);
}
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = log.bind(this, i);
}
for (var j = 0; j < 3; j++) {
funcs[j]();
}
用一个 立即调用函数表达式,最简单,最易读的方法来封装索引变量:
for (var i = 0; i < 3; i++) {
(function(index) {
console.log('iterator: ' + index);
//now you can also loop an ajax call here
//without losing track of the iterator value: $.ajax({});
})(i);
}
这将发送迭代器 i
进入我们定义为的匿名函数 index
。这会创建一个闭包,其中包含变量 i
保存以供以后在IIFE中的任何异步功能中使用。
派对迟到了,但我今天正在探讨这个问题,并注意到许多答案并没有完全解决Javascript如何处理范围,这基本上归结为这个问题。
正如许多其他人提到的那样,问题在于内部函数引用相同的内容 i
变量。那么为什么我们不在每次迭代时只创建一个新的局部变量,而是使用内部函数引用呢?
//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};
var funcs = {};
for (var i = 0; i < 3; i++) {
var ilocal = i; //create a new local variable
funcs[i] = function() {
console.log("My value: " + ilocal); //each should reference its own local variable
};
}
for (var j = 0; j < 3; j++) {
funcs[j]();
}
就像之前一样,每个内部函数输出分配给的最后一个值 i
,现在每个内部函数只输出分配给的最后一个值 ilocal
。但是,每次迭代都不应该拥有它 ilocal
?
事实证明,这就是问题所在。每次迭代都共享相同的范围,因此在第一次迭代之后的每次迭代都只是覆盖 ilocal
。从 MDN:
重要提示:JavaScript没有块范围。使用块引入的变量的范围限定为包含函数或脚本,并且设置它们的效果将持续超出块本身。换句话说,块语句不引入范围。虽然“独立”块是有效的语法,但您不希望在JavaScript中使用独立块,因为如果您认为它们在C或Java中执行类似块的操作,则它们不会按照您的想法执行。
重申强调:
JavaScript没有块范围。使用块引入的变量的范围限定为包含函数或脚本
我们可以通过检查看到这一点 ilocal
在我们在每次迭代中声明它之前:
//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};
var funcs = {};
for (var i = 0; i < 3; i++) {
console.log(ilocal);
var ilocal = i;
}
这正是这个bug如此棘手的原因。即使您重新声明变量,Javascript也不会抛出错误,JSLint甚至不会发出警告。这也是为什么解决这个问题的最好方法是利用闭包,这本质上是在Javascript中,内部函数可以访问外部变量,因为内部作用域“包围”外部作用域。
这也意味着内部函数“保持”外部变量并使它们保持活动,即使外部函数返回。为了利用这一点,我们创建并调用一个包装器函数,纯粹是为了创建一个新的范围,声明 ilocal
在新范围内,并返回一个使用的内部函数 ilocal
(以下更多解释):
//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};
var funcs = {};
for (var i = 0; i < 3; i++) {
funcs[i] = (function() { //create a new scope using a wrapper function
var ilocal = i; //capture i into a local var
return function() { //return the inner function
console.log("My value: " + ilocal);
};
})(); //remember to run the wrapper function
}
for (var j = 0; j < 3; j++) {
funcs[j]();
}
在包装器函数内部创建内部函数为内部函数提供了一个只有它才能访问的私有环境,即“闭包”。因此,每次我们调用包装函数时,我们都会创建一个新的内部函数,它具有自己独立的环境,确保了 ilocal
变量不会相互碰撞和覆盖。一些小的优化给出了许多其他SO用户给出的最终答案:
//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};
var funcs = {};
for (var i = 0; i < 3; i++) {
funcs[i] = wrapper(i);
}
for (var j = 0; j < 3; j++) {
funcs[j]();
}
//creates a separate environment for the inner function
function wrapper(ilocal) {
return function() { //return the inner function
console.log("My value: " + ilocal);
};
}
更新
随着ES6成为主流,我们现在可以使用新的 let
用于创建块范围变量的关键字:
//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};
var funcs = {};
for (let i = 0; i < 3; i++) { // use "let" to declare "i"
funcs[i] = function() {
console.log("My value: " + i); //each should reference its own local variable
};
}
for (var j = 0; j < 3; j++) { // we can use "var" here without issue
funcs[j]();
}
看看它现在有多容易!有关更多信息,请参阅 这个答案,我的信息是基于。
随着ES6现在得到广泛支持,这个问题的最佳答案已经改变。 ES6提供了 let
和 const
这个确切情况的关键字。我们可以使用,而不是搞乱闭包 let
设置一个循环范围变量,如下所示:
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function() {
console.log("My value: " + i);
};
}
val
然后将指向一个特定于该循环的特定转向的对象,并将返回正确的值而不使用额外的闭包表示法。这显然简化了这个问题。
const
类似于 let
有一个额外的限制,即在初始赋值后变量名不能反弹到新的引用。
现在,浏览器支持针对最新版本的浏览器。 const
/let
目前最新的Firefox,Safari,Edge和Chrome都支持。 Node也支持它,你可以利用像Babel这样的构建工具在任何地方使用它。你可以在这里看到一个有效的例子: http://jsfiddle.net/ben336/rbU4t/2/
文件在这里:
但请注意,在Edge 14支持之前IE9-IE11和Edge let
但得到上述错误(他们没有创造新的 i
每一次,所以上面的所有函数都会像我们使用的那样记录3 var
)。 Edge 14最终做对了。
另一种说法是 i
在您的函数中,在执行函数时绑定,而不是创建函数的时间。
当你创建闭包时, i
是对外部作用域中定义的变量的引用,而不是创建闭包时的副本。它将在执行时进行评估。
大多数其他答案提供了通过创建另一个不会为您更改值的变量来解决的方法。
我想我会添加一个清晰的解释。对于解决方案,个人而言,我会选择Harto,因为从这里的答案来看,这是最明智的方式。发布的任何代码都可以使用,但是我选择封闭工厂而不必编写一堆注释来解释为什么我要声明一个新变量(Freddy和1800's)或者有奇怪的嵌入式闭包语法(apphacker)。
你需要了解的是javascript中变量的范围是基于函数的。这是一个重要的区别,而不是c#,你有块范围,只是将变量复制到for内的一个将起作用。
将它包装在一个函数中,将函数评估为像apphacker的答案一样返回函数,这样做就可以了,因为变量现在具有函数范围。
还有一个let关键字而不是var,允许使用块范围规则。在这种情况下,在for中定义一个变量就可以了。也就是说,由于兼容性,let关键字不是一个实用的解决方案。
var funcs = {};
for (var i = 0; i < 3; i++) {
let index = i; //add this
funcs[i] = function() {
console.log("My value: " + index); //change to the copy
};
}
for (var j = 0; j < 3; j++) {
funcs[j]();
}
这是该技术的另一种变体,类似于Bjorn(apphacker),它允许您在函数内部分配变量值,而不是将其作为参数传递,有时可能更清晰:
for (var i = 0; i < 3; i++) {
funcs[i] = (function() {
var index = i;
return function() {
console.log("My value: " + index);
}
})();
}
请注意,无论您使用何种技术,都可以 index
变量变成一种静态变量,绑定到内部函数的返回副本。即,在调用之间保留对其值的更改。它可以非常方便。
这描述了在JavaScript中使用闭包的常见错误。
函数定义新环境
考虑:
function makeCounter()
{
var obj = {counter: 0};
return {
inc: function(){obj.counter ++;},
get: function(){return obj.counter;}
};
}
counter1 = makeCounter();
counter2 = makeCounter();
counter1.inc();
alert(counter1.get()); // returns 1
alert(counter2.get()); // returns 0
每一次 makeCounter
被调用, {counter: 0}
导致创建一个新对象。另外,新的副本 obj
也被创建以引用新对象。从而, counter1
和 counter2
是彼此独立的。
循环中的闭包
在循环中使用闭包很棘手。
考虑:
var counters = [];
function makeCounters(num)
{
for (var i = 0; i < num; i++)
{
var obj = {counter: 0};
counters[i] = {
inc: function(){obj.counter++;},
get: function(){return obj.counter;}
};
}
}
makeCounters(2);
counters[0].inc();
alert(counters[0].get()); // returns 1
alert(counters[1].get()); // returns 1
请注意 counters[0]
和 counters[1]
是 不 独立。事实上,他们的运作方式相同 obj
!
这是因为只有一个副本 obj
在循环的所有迭代中共享,可能是出于性能原因。
即使 {counter: 0}
在每次迭代中创建一个新对象,相同的副本 obj
只会更新一个
引用最新的对象。
解决方案是使用另一个辅助函数:
function makeHelper(obj)
{
return {
inc: function(){obj.counter++;},
get: function(){return obj.counter;}
};
}
function makeCounters(num)
{
for (var i = 0; i < num; i++)
{
var obj = {counter: 0};
counters[i] = makeHelper(obj);
}
}
这是有效的,因为函数作用域中的局部变量以及函数参数变量都是分配的 入境时的新副本。
有关详细讨论,请参阅 JavaScript关闭陷阱和使用
最简单的解决方案是,
而不是使用:
var funcs = [];
for(var i =0; i<3; i++){
funcs[i] = function(){
alert(i);
}
}
for(var j =0; j<3; j++){
funcs[j]();
}
提醒“2”,共3次。这是因为在for循环中创建的匿名函数,共享相同的闭包,并在该闭包中,值为 i
是一样的。使用它来防止共享关闭:
var funcs = [];
for(var new_i =0; new_i<3; new_i++){
(function(i){
funcs[i] = function(){
alert(i);
}
})(new_i);
}
for(var j =0; j<3; j++){
funcs[j]();
}
这背后的想法是,用for封装整个for循环体 IIFE (立即调用函数表达式)并传递 new_i
作为参数并将其捕获为 i
。由于匿名函数是立即执行的,所以 i
对于匿名函数内定义的每个函数,value是不同的。
这个解决方案似乎适合任何这样的问题,因为它需要对遇到此问题的原始代码进行最小的更改。事实上,这是设计,它应该不是一个问题!