循环内的 JavaScript 闭包——简单实用的例子

IT技术 javascript loops closures
2020-12-18 21:06:05

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

它输出这个:

我的value:3
我的value:3
我的value:3

而我希望它输出:

我的value:0
我的value:1
我的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();
}

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

6个回答

好吧,问题是i每个匿名函数内的变量都绑定到函数外的同一个变量。

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

但是请注意,Edge 14 之前的 IE9-IE11 和 Edge 支持let但会出现上述错误(它们不会i每次都创建一个新的,因此上面的所有函数都会像我们使用 那样记录 3 var)。Edge 14 终于做到了。


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


经典解决方案:闭包

您想要做的是将每个函数内的变量绑定到函数外的一个单独的、不变的值:

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”的值保持如您所愿。

不幸的是,这个答案已经过时,没有人会在底部看到正确的答案 -Function.bind()现在使用绝对是可取的,请参阅stackoverflow.com/a/19323214/785541
2021-02-07 21:06:05
@Wladimir:您的建议,即.bind()“正确的答案”是不正确的。他们每个人都有自己的位置。随着.bind()你没有绑定不能绑定的参数this值。此外,您还可以获得i参数的副本,但无法在调用之间对其进行变异,这有时是必需的。所以它们是完全不同的结构,更不用说.bind()实现在历史上一直很慢。当然,在这个简单的例子中,两者都可以,但闭包是一个需要理解的重要概念,这就是问题所在。
2021-02-09 21:06:05
@ChristianLandgren:只有在迭代数组时才有用。这些技术不是“黑客”。它们是必不可少的知识。
2021-02-11 21:06:05
是不是function createfunc(i) { return function() { console.log("My value: " + i); }; }仍然关闭,因为它使用变量i
2021-02-25 21:06:05
请停止使用这些 for-return 函数技巧,改用 [].forEach 或 [].map,因为它们可以避免重复使用相同的作用域变量。
2021-02-26 21:06:05

尝试:

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是现在做这种事情的最好方法。_.partial当您不需要或不想弄乱bind's时,还有 lo-dash/underscore's thisArg

立即调用函数表达式,又名 IIFE。(i) 是立即调用的匿名函数表达式的参数,索引从 i 开始设置。
2021-02-17 21:06:05
它实际上是在创建局部变量索引。
2021-02-18 21:06:05
关于的任何解释}(i));
2021-02-19 21:06:05
@aswzen 我认为它i作为参数传递index给函数。
2021-02-23 21:06:05

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

@squint @mekdev - 你们都是对的。我最初的例子是快速编写的,以演示如何bind使用。我根据您的建议添加了另一个示例。
2021-02-09 21:06:05
这也是我这些天所做的,我也喜欢 lo-dash/underscore 的 _.partial
2021-02-12 21:06:05
我认为不要在两个 O(n) 循环上浪费计算,只需执行 for (var i = 0; i < 3; i++) { log.call(this, i); }
2021-02-20 21:06:05
.bind() 执行已接受的答案所建议的 PLUS 对this.
2021-02-24 21:06:05
.bind()ECMAScript 6 特性将在很大程度上过时。此外,这实际上每次迭代都会创建两个函数。首先是匿名的,然后是由.bind(). 更好的用法是在循环外创建它,然后在循环.bind()内创建。
2021-03-02 21:06:05

使用立即调用函数表达式,这是包含索引变量的最简单和最易读的方法:

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

@Nico 在 OP 的特殊情况下,它们只是迭代数字,所以这不是一个很好的例子.forEach(),但是很多时候,当一个人从数组开始时,forEach()是一个不错的选择,例如:var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; });
2021-02-10 21:06:05
@Nico在原来的问题中显示的一样,只是你会使用index替代i
2021-02-21 21:06:05
@JLRishe var funcs = {}; for (var i = 0; i < 3; i++) { funcs[i] = (function(index) { return function() {console.log('iterator: ' + index);}; })(i); }; for (var j = 0; j < 3; j++) { funcs[j](); }
2021-03-01 21:06:05
您将如何使用这种技术来定义原始问题中描述的数组函数
2021-03-05 21:06:05
为了进一步提高代码可读性并避免混淆哪个i是什么,我将函数参数重命名为index.
2021-03-07 21:06:05

参加聚会有点晚,但我今天正在探索这个问题,并注意到许多答案并没有完全解决 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 中,内部函数可以访问外部变量的想法,因为内部作用域“包围”了外部作用域。

关闭

这也意味着内部函数“保留”外部变量并使它们保持活动状态,即使外部函数返回。为了利用这一点,我们创建和调用一个包装函数纯粹是为了创建一个新的作用域,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 现在成为主流,我们现在可以使用 newlet关键字来创建块范围的变量:

//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]();
}

看看现在是多么容易!有关更多信息,请参阅此答案,我的信息基于此答案

oop,抱歉,意思是说“您的示例中的代码似乎已经有效”
2021-02-12 21:06:05
现在在 JavaScript 中使用letconst关键字来定义块作用域如果这个答案扩展到包括它,在我看来它会在全球范围内更有用。
2021-02-18 21:06:05
@nuttyaboutnatty 抱歉回复这么晚。您的示例中的代码似乎尚未生效。您没有i在超时函数中使用,因此您不需要闭包
2021-02-18 21:06:05
@TinyGiant 确定,我添加了一些有关的信息let并链接了更完整的解释
2021-02-25 21:06:05
@ woojoo666莫非你的答案还调用两个交替URL的工作是在一个循环中,像这样:i=0; while(i < 100) { setTimeout(function(){ window.open("https://www.bbc.com","_self") }, 3000); setTimeout(function(){ window.open("https://www.cnn.com","_self") }, 3000); i++ }(可以用 getelementbyid 替换 window.open() ......)
2021-03-03 21:06:05