举个栗子 - JavaScript闭包内循环

coding2live 2021-01-08 11:00:06 85

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

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(var f of fns){
  f();
}

虽然这个问题很基础。。。但是有什么好的解决方法吗?

以下答案仅供参考

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

ES6提出的解决方案是使用:let

ECMAScript 6 (ES6)引入了新的let和const关键字,它们的作用域与使用 var 变量的作用域不同。
例如,循环中的index使用let进行了声明,那么循环中的每次迭代都将有一个具有循环作用域的新变量i,因此代码就会按预期运行。
可以参考这篇文章:http://www.2ality.com/2015/02/es6-scoping.html

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

但是要注意,IE9-IE11和Edge 14之前的浏览器对let的支持不完整,会让上面的结果出错(它们不会每次都创建一个新的i,所以上面所有的函数都会像我们使用var时一样会打印出3)。Edge14中是支持的。

ES5.1提供的一个解决方案:forEach

随着Array.prototype.forEach的广泛使用,在那些主要涉及对值数组进行迭代的情况下,.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"值不变。