JavaScript闭包内循环-简单的实用示例

var funcs = [];// let's create 3 functionsfor (var i = 0; i < 3; i++) {// and store them in funcsfuncs[i] = function() {// each should log its value.console.log("My value:", i);};}for (var j = 0; j < 3; j++) {// and now let's run each one to seefuncs[j]();}

它输出如下:

我的值:3
我的值:3
我的值:3

而我希望它输出:

我的值:0
我的值:1
我的值:2


当运行函数的延迟是由使用事件侦听器引起时,也会出现同样的问题:

var buttons = document.getElementsByTagName("button");// let's create 3 functionsfor (var i = 0; i < buttons.length; i++) {// as event listenersbuttons[i].addEventListener("click", function() {// each should log its value.console.log("My value:", i);});}
<button>0</button><br /><button>1</button><br /><button>2</button>

…或异步代码,例如使用Promises:

// Some async wait functionconst wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));
for (var i = 0; i < 3; i++) {// Log `i` as soon as each promise resolves.wait(i * 100).then(() => console.log(i));}

for infor of循环中也很明显:

const arr = [1,2,3];const fns = [];
for (var i in arr){fns.push(() => console.log("index:", i));}
for (var v of arr){fns.push(() => console.log("value:", v));}
for (const n of arr) {var obj = { number: n }; // or new MyLibObject({ ... })fns.push(() => console.log("n:", n, "|", "obj:", JSON.stringify(obj)));}
for(var f of fns){f();}

这个基本问题的解决方案是什么?

463201 次浏览

试试:

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 的 关于使用 .bind的最新答案是现在做这类事情的最好方式。当您不需要或不想干扰 bindthisArg时,还有低划线/下划线的 _.partial

问题是,在每个匿名函数中的变量 i都绑定到函数之外的同一个变量。

ES6解决方案: let

ECMAScript 6(ES6)引入了与基于 var的变量作用域不同的新的 letconst关键字。例如,在具有基于 let的索引的循环中,通过循环的每次迭代都将有一个具有循环作用域的新变量 i,因此您的代码将如您所期望的那样工作。有许多资源,但我建议 2ality 的块范围站作为一个伟大的信息来源。

for (let i = 0; i < 3; i++) {
funcs[i] = function() {
console.log("My value: " + i);
};
}

不过要注意的是,IE9-IE11和 Edge 14之前的 Edge 都支持 let,但是上面的方法是错误的(它们不会每次都创建一个新的 i,所以上面的所有函数都会像使用 var那样记录3)。边缘14终于做对了。


ES5.1解决方案:

随着 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()函数也提供了类似的功能。


经典解决方案: 闭合

你需要做的就是将每个函数中的变量绑定到函数之外的一个单独的、不变的值:

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++) {
// and now let's run each one to see
funcs[j]();
}

由于 JavaScript 函数作用域中没有块作用域——通过将函数创建包装在一个新函数中,您可以确保“ i”的值保持原样。

原始示例无法工作的原因是,在循环中创建的所有闭包都引用了相同的框架。实际上,在一个对象上有3个方法,只有一个 i变量。他们都打印出了相同的数值。

你需要理解的是 javascript 中变量的作用域是基于函数的。这是一个重要的区别,比如 c # 中有块作用域,只需将变量复制到 for 中的一个就可以了。

将它包装在一个函数中,该函数像 apphacker 的答案那样计算返回的函数,这样就可以解决这个问题,因为变量现在具有了函数作用域。

还有一个 let 关键字代替 var,它允许使用 block scope 规则。在这种情况下,在 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]();
}

另一种说法是,函数中的 i是在执行函数时绑定的,而不是在创建函数时绑定的。

创建闭包时,i是对在外部作用域中定义的变量的引用,而不是创建闭包时的变量副本。它将在执行时进行计算。

其他大多数答案都提供了一些解决方法,即创建另一个不会为您更改值的变量。

我只是想给你一个解释。对于一个解决方案,我个人会选择哈托的,因为从这里的答案来看,这是最不言而喻的解决方法。发布的任何代码都可以工作,但是我宁愿选择闭包工厂,也不愿意写一堆注释来解释为什么我要声明一个新变量(Freddy and 1800’s)或者使用奇怪的嵌入式闭包语法(apphacker)。

这里还有一个类似于 Bjorn (apphacker)技术的变体,它允许你在函数内部赋值,而不是将其作为参数传递,这有时候可能会更清楚:

var funcs = [];
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}都会创建一个新对象 因此,counter1counter2是相互独立的。

循环中的闭包

在循环中使用闭包是很棘手的。

考虑一下:

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的相同副本也只是用 对最新对象的引用。

解决方案是使用另一个 helper 函数:

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);
}
}

这是因为直接分配了函数作用域中的局部变量以及函数参数变量 新复印件。

随着 ES6现在得到广泛支持,这个问题的最佳答案已经发生了变化。ES6为这种情况提供了 letconst关键字。我们可以使用 let来设置一个循环范围变量,像下面这样:

var funcs = [];


for (let i = 0; i < 3; i++) {
funcs[i] = function() {
console.log("My value: " + i);
};
}

然后,val将指向一个特定于循环的特定回合的对象,并且将返回正确的值,而不使用附加的闭包符号。这显然极大地简化了这个问题。

const类似于 let,但有一个额外的限制,即变量名不能在初始赋值后反弹到新的引用。

浏览器支持现在在这里为那些针对最新版本的浏览器。目前最新的 Firefox、 Safari、 Edge 和 Chrome 都支持 const/let。Node 也支持它,您可以利用诸如 Babel 之类的构建工具在任何地方使用它。您可以在这里看到一个工作示例: http://jsfiddle.net/ben336/rbU4t/2/

这里的文档:

不过要注意的是,IE9-IE11和 Edge 14之前的 Edge 都支持 let,但是上面的方法是错误的(它们不会每次都创建一个新的 i,所以上面的所有函数都会像使用 var那样记录3)。边缘14终于做对了。

最简单的解决办法就是,

不使用:

var funcs = [];
for(var i =0; i<3; i++){
funcs[i] = function(){
alert(i);
}
}


for(var j =0; j<3; j++){
funcs[j]();
}

提醒“2”三次。这是因为在 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 循环的整个主体,并将 new_i作为一个参数传递,然后将其捕获为 i。由于匿名函数是立即执行的,因此对于匿名函数中定义的每个函数,i值都是不同的。

这个解决方案似乎适合任何这样的问题,因为它只需要对遭受这个问题的原始代码进行最小的更改。事实上,这是设计好的,根本不应该成为一个问题!

试试这个短一点的

  • 没有数组

  • 没有多余的 for 循环


for (var i = 0; i < 3; i++) {
createfunc(i)();
}


function createfunc(i) {
return function(){console.log("My value: " + i);};
}

Http://jsfiddle.net/7p6en/

另一种尚未被提及的方法是使用 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]();
}

更新

正如@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 中的任何异步功能中使用。

OP 显示的代码的主要问题是,直到第二个循环才读取 i。为了演示,假设在代码中看到一个错误

funcs[i] = function() {            // and store them in funcs
throw new Error("test");
console.log("My value: " + i); // each should log its value.
};

在执行 funcs[someIndex]之前,这个错误实际上不会发生。使用同样的逻辑,很明显,直到这一点之前也不会收集 i的值。一旦原始循环结束,i++i带到 3的值,这导致条件 i < 3失败和循环结束。在这一点上,i3,因此当使用 funcs[someIndex](),并且 i被评估时,它是3-每次。

为了克服这个问题,您必须在遇到 i时对它进行评估。注意,这已经以 funcs[i]的形式发生了(其中有3个唯一的索引)。有几种方法可以获取这个值。一种是将它作为一个参数传递给函数,这里已经用几种方式显示了这个函数。

另一种选择是构造一个函数对象,它将能够关闭变量。可以这样做

jsFiddle Demo

funcs[i] = new function() {
var closedVariable = i;
return function(){
console.log("My value: " + closedVariable);
};
};

下面是一个使用 forEach(回到 IE9)的简单解决方案:

var funcs = [];
[0,1,2].forEach(function(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
}

印刷品:

My value: 0
My value: 1
My value: 2

在阅读了各种解决方案之后,我想补充一点,这些解决方案之所以有效,是因为它们依赖于 范围链的概念。这是 JavaScript 在执行过程中解析变量的方式。

  • 每个函数定义形成一个由所有局部 由 var及其 arguments声明的变量。
  • 如果在另一个(外部)函数中定义了内部函数,则 形成一个链,并将在执行期间使用
  • 当一个函数被执行时,运行时通过搜索 范围链来计算变量。如果可以在链的某个点找到变量,它将停止搜索并使用它,否则它将继续搜索,直到到达属于 window的全局作用域。

在初始代码中:

funcs = {};
for (var i = 0; i < 3; i++) {
funcs[i] = function inner() {        // function inner's scope contains nothing
console.log("My value: " + i);
};
}
console.log(window.i)                  // test value 'i', print 3

当执行 funcs时,范围链将是 function inner -> global。由于变量 i不能在 function inner中找到(既不使用 var声明,也不作为参数传递) ,因此它继续搜索,直到最终在全局作用域 window.i中找到 i的值。

通过将其包装在一个外部函数中,可以像 Harto那样显式地定义一个 helper 函数,也可以像 比约恩那样使用一个匿名函数:

funcs = {};
function outer(i) {              // function outer's scope contains 'i'
return function inner() {      // function inner, closure created
console.log("My value: " + i);
};
}
for (var i = 0; i < 3; i++) {
funcs[i] = outer(i);
}
console.log(window.i)          // print 3 still

当执行 funcs时,范围链将为 function inner -> function outer。这次 i可以在外部函数的作用域中找到,在 for 循环中执行3次,每次都正确地绑定了值 i。在内部执行时,它不会使用 window.i的值。

更多的细节可以找到 给你
它包括在循环中创建闭包的常见错误,就像我们在这里看到的那样,以及为什么我们需要闭包和性能考虑。

我很惊讶还没有人建议使用 forEach函数来更好地避免(重新)使用局部变量。事实上,由于这个原因,我不再使用 for(var i ...)了。

[0,2,3].forEach(function(i){ console.log('My value:', i); });
// My value: 0
// My value: 2
// My value: 3

//编辑为使用 forEach而不是 map。

虽然有点晚了,但是我今天探讨了这个问题,并注意到许多答案并没有完全解决 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;
}

这就是为什么这个窃听器这么棘手。即使重新声明变量,Javascript 也不会抛出错误,JSLint 甚至不会抛出警告。这也是为什么解决这个问题的最好方法是利用闭包,在 Javascript 中,内部函数可以访问外部变量,因为内部作用域“封闭”了外部作用域。

Closures

这也意味着,即使外部函数返回,内部函数也“保持”外部变量并使它们保持活动状态。为了利用这一点,我们创建并调用一个包装函式,纯粹是为了创建一个新作用域,在新作用域中声明 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]();
}

看看它现在是多么容易! 更多的信息见 这个答案,我的信息是基于。

这是异步代码经常遇到的问题,变量 i是可变的,在函数调用的时候,使用 i的代码将被执行,并且 i将突变为它的最后一个值,这意味着在循环中创建的所有函数将创建一个 了结,而 i将等于3(for循环的上限 + 1)。

解决这个问题的方法是创建一个函数,该函数将保存每次迭代的 i值,并强制复制 i(因为它是一个原语,如果有帮助的话,可以将它看作一个快照)。

您可以对诸如 Query-js(*)之类的数据列表使用声明性模块。在这些情况下,我个人发现陈述式方法不那么令人惊讶

var funcs = Query.range(0,3).each(function(i){
return  function() {
console.log("My value: " + i);
};
});

然后,您可以使用第二个循环并得到预期的结果,或者您可以这样做

funcs.iterate(function(f){ f(); });

(*)我是 query-js 的作者,因此我倾向于使用 query-js,所以不要把我的话仅仅当作对声明式方法的推荐:)

我更喜欢使用 forEach函数,它有自己的闭包,可以创建伪范围:

var funcs = [];


new Array(3).fill(0).forEach(function (_, i) { // creating a range
funcs[i] = function() {
// now i is safely incapsulated
console.log("My value: " + i);
};
});


for (var j = 0; j < 3; j++) {
funcs[j](); // 0, 1, 2
}

这看起来比其他语言中的范围更丑陋,但恕我直言,它没有其他解决方案那么可怕。

还有另一个解决方案: 不要创建另一个循环,只需将 this绑定到返回函数。

var funcs = [];


function createFunc(i) {
return function() {
console.log('My value: ' + i); //log value of i.
}.call(this);
}


for (var i = 1; i <= 5; i++) {  //5 functions
funcs[i] = createFunc(i);     // call createFunc() i=5 times
}

通过绑定 这个,也解决了这个问题。

首先,理解这段代码的错误之处:

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
}

这里当 funcs[]阵列被初始化时,i被递增,funcs阵列被初始化,func阵列的大小变成3,所以 i = 3,。 现在,当调用 funcs[j]()时,它再次使用变量 i,该变量已经增加到3。

为了解决这个问题,我们有很多选择,下面是其中的两个:

  1. 我们可以用 let初始化 i,或者用 let初始化一个新的变量 index,使它等于 i。因此,当进行调用时,将使用 index,其作用域将在初始化后结束。对于调用,index将再次初始化:

    var funcs = [];
    for (var i = 0; i < 3; i++) {
    let index = i;
    funcs[i] = function() {
    console.log("My value: " + index);
    };
    }
    for (var j = 0; j < 3; j++) {
    funcs[j]();
    }
    
  2. Other Option can be to introduce a tempFunc which returns the actual function:

    var funcs = [];
    function tempFunc(i){
    return function(){
    console.log("My value: " + i);
    };
    }
    for (var i = 0; i < 3; i++) {
    funcs[i] = tempFunc(i);
    }
    for (var j = 0; j < 3; j++) {
    funcs[j]();
    }
    

你的代码不起作用,因为它的作用是:

Create variable `funcs` and assign it an empty array;
Loop from 0 up until it is less than 3 and assign it to variable `i`;
Push to variable `funcs` next function:
// Only push (save), but don't execute
**Write to console current value of variable `i`;**


// First loop has ended, i = 3;


Loop from 0 up until it is less than 3 and assign it to variable `j`;
Call `j`-th function from variable `funcs`:
**Write to console current value of variable `i`;**
// Ask yourself NOW! What is the value of i?

现在的问题是,当函数被调用时,变量 i的值是多少?因为第一个循环是使用 i < 3条件创建的,所以当条件为 false 时它立即停止,因此它是 i = 3

您需要明白,在创建函数时,它们的代码都不会执行,只会保存到以后使用。因此,当稍后调用它们时,解释器执行它们并问: “ i的当前值是多少?”

因此,您的目标是首先将 i的值保存到函数中,然后再将函数保存到 funcs中。可以这样做,例如:

var funcs = [];
for (var i = 0; i < 3; i++) {          // let's create 3 functions
funcs[i] = function(x) {            // and store them in funcs
console.log("My value: " + x); // each should log its value.
}.bind(null, i);
}
for (var j = 0; j < 3; j++) {
funcs[j]();                        // and now let's run each one to see
}

这样,每个函数都有自己的变量 x,我们在每次迭代中将这个 x设置为 i的值。

这只是解决这个问题的多种方法之一。

通过 ES6块级别的新特性管理范围:

var funcs = [];
for (let 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 (let j = 0; j < 3; j++) {
funcs[j]();                        // and now let's run each one to see
}

OP 问题中的代码被替换为 < a href = “ https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statments/let”rel = “ noReferrer”> let 而不是 var

让我们利用 new Function。因此,i不再是 了结的变量,而只是文本的一部分:

var funcs = [];
for (var i = 0; i < 3; i++) {
var functionBody = 'console.log("My value: ' + i + '");';
funcs[i] = new Function(functionBody);
}


for (var j = 0; j < 3; j++) {
funcs[j]();
}

JavaScript 函数在声明时“关闭”它们可以访问的作用域,并保留对该作用域的访问,即使该作用域中的变量发生了变化。

var funcs = []


for (var i = 0; i < 3; i += 1) {
funcs[i] = function () {
console.log(i)
}
}


for (var k = 0; k < 3; k += 1) {
funcs[k]()
}

上面数组中的每个函数都关闭全局作用域(全局,仅仅是因为它们恰好是在全局作用域中声明的)。

稍后将调用这些函数,并记录全局作用域中 i的最新值。这就是结束的魔力和挫折。

“ JavaScript 函数关闭它们在其中声明的作用域,并保留对该作用域的访问,即使该作用域内的变量值发生了变化。”

使用 let而不是 var可以解决这个问题,因为每次运行 for循环时都会创建一个新的作用域,为每个函数创建一个单独的作用域来关闭。其他各种技术使用额外的函数做同样的事情。

var funcs = []


for (let i = 0; i < 3; i += 1) {
funcs[i] = function () {
console.log(i)
}
}


for (var k = 0; k < 3; k += 1) {
funcs[k]()
}

(let使变量块作用域。块由大括号表示,但是在 for 循环的情况下,初始化变量 i在我们的例子中被认为是在大括号中声明的。)

使用 了结结构,这将减少额外的 for 循环:

var funcs = [];
for (var i = 0; i < 3; i++) {
(funcs[i] = function() {
console.log("My value: " + i);
})(i);
}

许多解决方案似乎是正确的,但是他们没有提到它叫做 Currying,这是一种针对这种情况的函数式编程设计模式。比绑定快3-10倍,具体取决于浏览器。

var funcs = [];
for (var i = 0; i < 3; i++) {      // let's create 3 functions
funcs[i] = curryShowValue(i);
}
for (var j = 0; j < 3; j++) {
funcs[j]();                      // and now let's run each one to see
}


function curryShowValue(i) {
return function showValue() {
console.log("My value: " + i);
}
}

参见 不同浏览器的性能提高

计数器是一个原始人

让我们将回调函数定义如下:

// ****************************
// COUNTER BEING A PRIMITIVE
// ****************************
function test1() {
for (var i=0; i<2; i++) {
setTimeout(function() {
console.log(i);
});
}
}
test1();
// 2
// 2

超时完成后,它将为两者打印2。这是因为回调函数根据定义函数的 词法范围词法范围访问该值。

为了在定义回调时传递和保留该值,我们可以创建一个 了结,以便在调用回调之前保留该值。可以这样做:

function test2() {
function sendRequest(i) {
setTimeout(function() {
console.log(i);
});
}


for (var i = 0; i < 2; i++) {
sendRequest(i);
}
}
test2();
// 1
// 2

这里的特别之处在于“原语是通过值传递和复制的。因此,当定义闭包时,它们保留前一个循环的值。”

计数器成为一个物体

因为闭包可以通过引用访问父函数变量,所以这种方法与原语不同。

// ****************************
// COUNTER BEING AN OBJECT
// ****************************
function test3() {
var index = { i: 0 };
for (index.i=0; index.i<2; index.i++) {
setTimeout(function() {
console.log('test3: ' + index.i);
});
}
}
test3();
// 2
// 2

因此,即使为作为对象传递的变量创建了闭包,也不会保留循环索引的值。这表明对象的值不会被复制,而是通过引用访问。

function test4() {
var index = { i: 0 };
function sendRequest(index, i) {
setTimeout(function() {
console.log('index: ' + index);
console.log('i: ' + i);
console.log(index[i]);
});
}


for (index.i=0; index.i<2; index.i++) {
sendRequest(index, index.i);
}
}
test4();
// index: { i: 2}
// 0
// undefined


// index: { i: 2}
// 1
// undefined

这个问题已经有很多有效的答案了。不过,使用函数式方法的人并不多。下面是一个使用 forEach方法的替代解决方案,它可以很好地处理回调和闭包:

let arr = [1,2,3];

让 myFunc = (val,index) = > { Log (‘ val:’+ val +’nindex:’+ index) ; } ;

arr.forEach(myFunc);

这个问题真正展示了 JavaScript 的历史!现在我们可以避免使用箭头函数进行块范围界定,并使用 Object 方法直接处理来自 DOM 节点的循环。

const funcs = [1, 2, 3].map(i => () => console.log(i));
funcs.map(fn => fn())

const buttons = document.getElementsByTagName("button");
Object
.keys(buttons)
.map(i => buttons[i].addEventListener('click', () => console.log(i)));
<button>0</button><br>
<button>1</button><br>
<button>2</button>

我们将检查声明 varlet时实际发生的情况 一个接一个。

Case1 : < strong > 使用 var

<script>
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function () {
debugger;
console.log("My value: " + i);
};
}
console.log(funcs);
</script>

现在通过按 F12打开 铬合金控制台窗口并刷新页面。 展开数组中的每3个函数。你会看到一个名为 [[Scopes]].Expand 的属性。你会看到一个 名为 "Global"的数组对象,展开这个对象。您将发现声明到对象中的属性 'i',其值为3。

enter image description here

enter image description here

结论:

  1. 当您在函数之外使用 'var'声明一个变量时,它就变成了全局变量(您可以通过键入 i或 控制台窗口中的 window.i,它将返回3)。
  2. 声明的匿名函数不会调用和检查函数内的值,除非调用 功能。
  3. 在调用该函数时,console.log("My value: " + i)从其 Global对象获取值并显示 结果。

使用 let

现在用 'let'代替 'var'

<script>
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function () {
debugger;
console.log("My value: " + i);
};
}
console.log(funcs);
</script>

做同样的事情,去看望远镜。现在您将看到两个对象 "Block""Global"。现在展开 Block对象,您 将会看到这里定义了‘ i’,奇怪的是,对于每个函数,如果 i不同(0,1,2) ,值就不同。

enter image description here

结论:

即使在函数外部但在循环内部使用 'let'声明变量,这个变量也不是全局变量 变量,它将成为一个 Block级别的变量,它只对同一个函数可用。这就是原因,我们 当我们调用这些函数时,每个函数的 i值都不同。

关于如何更接近工作的更多细节,请通过令人敬畏的视频教程 https://youtu.be/71AtaJpJHw0

虽然这个问题已经过时并且得到了回答,但我还有另一个相当有趣的解决方案:

var funcs = [];


for (var i = 0; i < 3; i++) {
funcs[i] = function() {
console.log("My value: " + i);
};
}


for (var i = 0; i < 3; i++) {
funcs[i]();
}

变化如此之小,几乎很难看到我做了什么。我把第二个迭代器从 j 改为 i。这以某种方式及时刷新 i 的状态,以便给出期望的结果。我这样做是偶然的,但考虑到以前的答案,这是有道理的。

我写这篇文章是为了指出这个很小但很重要的区别。希望这能帮助像我一样的其他学习者解决一些困惑。

注意: 我不分享这个,因为我认为这是正确的答案。这是一个不稳定的解决方案,在某些情况下可能会崩溃。事实上,我很惊讶它真的有用。

假设你不使用 ES6; 您可以使用 IIFE:

var funcs = [];
for (var i = 0; i < 13; i++) {
funcs[i] = (function(x) {
console.log("My value: " + i)
})(i);
}

但情况会有所不同。

好吧。我把所有的答案都读了一遍。即使这里有一个很好的解释-我只是不能让这个工作。所以我上网查了一下。在 https://dzone.com/articles/why-does-javascript-loop-only-use-last-value的人有一个答案,这里没有提出。所以我想我应该发一个简短的例子。这对我来说更有意义。

它的长处和短处在于,LET 命令很好,但是现在才开始使用。但是,LET 命令实际上只是一个 TRY-CATCH 组合。这可以追溯到 IE3(我相信)。使用 TRY-CATCH 组合寿命是简单和良好的。也许这就是 EMCScript 的人决定使用它的原因。它也不需要 setTimeout ()函数。所以不会浪费时间。基本上,每个 FOR 循环需要一个 TRY-CATCH 组合。这里有一个例子:

    for( var i in myArray ){
try{ throw i }
catch(ii){
// Do whatever it is you want to do with ii
}
}

如果有多个 FOR 循环,则只需为每个 FOR 循环放置 TRY-CATCH 组合。而且,就我个人而言,我总是使用任何我正在使用的 FOR 变量的双重字母。所以“ ii”代表“ i”等等。我在一个例程中使用这种技术将 mouseover 命令发送到另一个例程。

为什么不直接在第一个(也是唯一的)循环中调用刚刚创建的每个函数,比如:

 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.
};
funcs[i]();// and now let's run each one to see
}
var funcs = [];
for (var i = 0; i < 3; i++) {      // let's create 3 functions
funcs[i] = function(param) {          // and store them in funcs
console.log("My value: " + param); // each should log its value.
};
}
for (var j = 0; j < 3; j++) {
funcs[j](j);                      // and now let's run each one to see with j
}

这证明了 javascript 在“闭包”和“非闭包”的工作方式方面是多么丑陋。

在下列情况下:

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.
};
}

Funs [ i ]是一个全局函数,而‘ console.log (“ My value:”+ i) ;”正在打印全局变量 i

在这种情况下

var funcs = [];


function createfunc(i) {
return function() { console.log("My value: " + i); };
}


for (var i = 0; i < 3; i++) {
funcs[i] = createfunc(i);
}

由于 javascript 的这种扭曲的闭包设计,‘ console.log (“ My value:”+ i) ;’正在从外部函数‘ createfunc (i)’打印 i

这一切都是因为 javascript 不能像 C 编程语言那样在函数内部设计像“ static”变量这样体面的东西!

只需将 var 关键字改为 let。

Var 是函数作用域。

让我们进行块作用域。

当您开始编码时,for 循环将迭代并将 i 的值赋给3,这将在整个代码中保持为3。我建议您阅读更多关于 node (let、 var、 const 和其他)中的作用域的信息

funcs = [];
for (let i = 0; i < 3; i++) {      // let's create 3 functions
funcs[i] =async function() {          // and store them in funcs
await 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
}
  asyncIterable = [1,2,3,4,5,6,7,8];


(async function() {
for await (let num of asyncIterable) {
console.log(num);
}
})();

使用 let (block-scope)代替 var。

var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function() {
console.log("My value: " + i);
};
}
for (var j = 0; j < 3; j++) {
funcs[j]();
}

ES6的支持下,实现这一点的最佳方法是在这种情况下使用 letconst关键字。所以 var变量得到 hoisted,在循环结束时,i的值得到更新,对于所有的 closures... ,我们可以只使用 let来设置一个循环范围变量,如下所示:

var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function() {
console.log("My value: " + i);
};
}

直到 ES5,这个问题只能用 了结来解决。

但是现在在 ES6中,我们有块级作用域变量。在第一个 循环中将 Var改为 可以解决这个问题。

var funcs = [];
for (let 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
}

例如,如果你遇到的是 while循环而不是 for循环的问题:

var i = 0;
while (i < 5) {
setTimeout(function() {
console.log(i);
}, i * 1000);
i++;
}

关闭当前值的技术稍有不同。在 while块中声明一个块作用域变量,使用 const,并将当前的 i赋给它。然后,在异步使用变量的地方,用新的块作用域变量替换 i:

var i = 0;
while (i < 5) {
const thisIterationI = i;
setTimeout(function() {
console.log(thisIterationI);
}, i * 1000);
i++;
}

对于不支持块作用域变量的老版本浏览器,可以使用一个名为 IIFE 的 i:

var i = 0;
while (i < 5) {
(function(innerI) {
setTimeout(function() {
console.log(innerI);
}, innerI * 1000);
})(i);
i++;
}

如果要调用的异步操作碰巧是像上面那样的 setTimeout,您也可以使用 第三参数调用 setTimeout,以指示调用传递函数的参数:

var i = 0;
while (i < 5) {
setTimeout(
(thisIterationI) => { // Callback
console.log(thisIterationI);
},
i * 1000, // Delay
i // Gets passed to the callback; becomes thisIterationI
);
i++;
}