一尘不染

循环内的JavaScript闭合–简单的实际示例

javascript

var funcs = [];

// let's create 3 functions

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

  // and store them in funcs

  funcs[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 see

  funcs[j]();

}

它输出:

My value: 3
My value: 3
My value: 3

而我希望它输出:

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


当由于使用事件侦听器而导致功能运行延迟时,会发生相同的问题:

var buttons = document.getElementsByTagName("button");

// let's create 3 functions

for (var i = 0; i < buttons.length; i++) {

  // as event listeners

  buttons[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 function

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

}

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


阅读 615

收藏
2020-04-22

共1个答案

一尘不染

嗯,问题在于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++) {

  // and now let's run each one to see

  funcs[j]();

}

由于JavaScript中没有块作用域-只有函数作用域-通过将函数创建包装在新函数中,因此请确保“ i”的值保持预期。


ES5.1解决方案: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)引入了新的letconst关键字,其作用域与var基于变量的作用域不同。例如,在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每次都创建一个新的,因此上面的所有功能都将记录3,就像我们使用一样var)。Edge
14终于正确了。

2020-04-22