在 Javascript 中的 for 循环中关闭,结果令人困惑

Closure inside a for Loop in Javascript with confusing results

我是JavaScript的初学者,我已经阅读了类似主题中的每个答案,但我仍然无法理解到底发生了什么,因为没有人解释我感到困惑的部分。

我有一个包含两个段落的 HTML 文档,这就是我在单击它时将段落颜色更改为 red 的方法:

var func = function() {
/*Line 1*/ var paragraphs = document.querySelectorAll('p')
/*Line 2*/
/*Line 3*/ for (var i = 0; i < paragraphs.length; i++) {
/*Line 4*/    p = paragraphs[i]
/*Line 5*/    p.addEventListener('click', function() {
/*Line 6*/      p.classList.toggle('red')
/*Line 7*/    })
/*Line 8*/ }
}
func();

结果是,无论我点击哪里,只有最后一段的颜色变为红色。所有和我类似问题的答案都说For循环结束后,i的值会变成1,所以闭包会用到,然后eventListener 将添加到同一第二段?而且我可以使用 Immediately-Invoked Functionlet 来解决这个问题,使 i 对闭包私有,我不知道为什么我应该将 ì 设为私有,如果闭包有在循环的每次迭代中访问它..

我就是不明白这是怎么回事,循环不是一行一行的执行的吗?在开始时 i 的值为 0,因此在 Line 4 处变量 p 将包含第一段,然后在 Line 5-6 处函数将使用p 并将侦听器附加到它,然后第二次执行循环并且 i 将具有 1 的值,然后在 Line 5-6 处再次获得闭包p 的新值 ?

我知道闭包可以像 i 一样访问全局变量,所以当它的值改变时它可以访问 Line 4 处的 p

请问我在这里遗漏了什么?非常感谢您!

闭包是一个关闭变量值的函数,因此它们不会在循环的下一次迭代中发生变化,这发生在 click 发生之前等等。

var paragraphs = document.querySelectorAll('p');

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

    (function(p) { // <- any function call, would create a new scope, and hence a closure
        p.addEventListener('click', function() {
            p.classList.toggle('red'); // note that "this" would work here, instead of "p"
        });
    })(paragraphs[i]); // <- in this case, it's an IIFE, but it doesn't have to be

}

那将是一个闭包

在 JavaScript(ECMA-Script 5 及更早版本)中,只有 函数 可以创建作用域。

另一方面,闭包不捕获变量值。也就是说,你需要自己做,正如你已经说过的使用 IIFE(立即调用的函数表达式):

for (var i = 0; i < paragraphs.length; i++) {
  (function(p) {
    p.addEventListener('click', function() {
      p.classList.toggle('red')
    })
  })(paragraphs[i]);
}

顺便说一句,谁知道您是否可以使用 document.querySelectorAll:

来简化此代码
// Now you don't need IIFEs anymore...
// Change the "p" selector with whatever CSS selector
// that might fit better in your scenario...
Array.from(document.querySelectorAll("p"))
  .forEach(function(p) {
    p.addEventListener("click", function() {
      p.classList.toggle('red');
    });
  });

好吧,事实上,您可以重构代码以使用 Array.prototype.forEach 而您也不需要使用 IIFE:

paragraphs.forEach(function(p) {
    p.addEventListener("click", function() {
      p.classList.toggle('red');
    });
});

这是一个经典问题,由您的变量 p 绑定到一个变量,该变量的值在调用回调时已更改。

为了遍历数组并调用异步代码而无需任何特殊技巧,您可以简单地使用 Array.prototype.forEach(在某些浏览器上 classList 属性 也支持此方法):

paragraphs.forEach(function(p) {
    p.addEventListener('click', function() {
         p.classList.toggle('red');
    });
});

由于 pforEach 回调的 绑定参数,因此它始终保持当前迭代的预期值 ,以及带有切换的正确元素。

如果您的浏览器支持classList.forEach,那么使用:

[].prototype.forEach.call(paragraphs, ...);

您展示的是众所周知的闭包示例....

一开始可能很难理解

/*Line 1*/ var paragraphs = document.querySelectorAll('p')
/*Line 2*/
/*Line 3*/ for (var i = 0; i < paragraphs.length; i++) {
/*Line 4*/    p = paragraphs[i]
/*Line 5*/    p.addEventListener('click', function() {
/*Line 6*/      p.classList.toggle('red')
/*Line 7*/    })
/*Line 8*/ }

第 5、6 和 7 行包含存储在每个段落中的匿名回调函数。该函数依赖于父函数中的变量 i,因为内部函数使用 p,定义为 paragraphs[i]。因此,即使您没有在内部函数中显式使用 i,您的 p 变量也是如此。假设文档中有 7 个段落...因此,您有 7 个函数 "closed" 围绕一个 i 变量。 i 父函数终止时不能超出范围,因为 7 个函数需要它。因此,当有人点击其中一个段落时,循环已经完成(i 现在将是 8)并且每个函数都在查看相同的 i 值。

为了解决这个问题,点击回调函数需要各自获取自己的值,而不是共享一个。这可以通过多种方式实现,但它们都涉及将 i 的副本传递到点击回调函数中,以便 i 值的副本将存储在每个点击回调函数中或删除i 一起使用。仍然会有闭包,但不会有您最初遇到的副作用,因为嵌套函数不会依赖父函数中的变量。

下面是一个从嵌套函数中删除 i 的示例,从而解决了问题:

var paragraphs = document.querySelectorAll('p')

for (var i = 0; i < paragraphs.length; i++) {
    paragraphs[i].addEventListener('click', function() {
      this.classList.toggle('red')
    });
}
.red {color:red;}
<p>Paragraph</p>
<p>Paragraph</p>
<p>Paragraph</p>
<p class="red">Paragraph</p>
<p>Paragraph</p>
<p>Paragraph</p>