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

时间:2016-11-02 20:59:01

标签: javascript for-loop closures iife

我是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,这就是闭包将使用的内容,然后是{{ 1}}会被添加到同一个第二段吗?我可以使用eventListenerImmediately-Invoked Function来解决这个问题,以使let成为封闭的私有,我不知道为什么如果封闭有私有i私有在循环的每次迭代中访问它..

我只是无法理解这里到底发生了什么,是不是循环执行了一行又一个?开始时ì的值为i,因此在0变量Line 4将包含第一段,然后在p该函数将使用那个Line 5-6并将监听器附加到它,然后第二次执行循环,p的值为i,然后再次1,闭包得到新值Line 5-6

我知道闭包可以像p一样访问全局变量,因此当它的值发生变化时,它可以i访问p

我在这里想念的是什么?非常感谢你提前!

4 个答案:

答案 0 :(得分:2)

您正在展示众所周知的封闭示例......

这首先很难掌握

/*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个函数已经关闭&#34;围绕一个i变量。当父函数终止时,i不能超出范围,因为7个函数需要它。因此,当人类点击其中一个段落时,循环已完成(i现在将为8)并且每个函数都在查看相同的i值。

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

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

&#13;
&#13;
var paragraphs = document.querySelectorAll('p')

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

答案 1 :(得分:1)

闭包是一个关闭变量值的函数,因此它们不会在循环的下一次迭代中发生变化,这发生在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

}

这将是一个闭包

答案 2 :(得分:1)

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

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

TextView

BTW谁知道您是否可以使用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'); }); }); ,但您也不需要使用IIFE:

Array.prototype.forEach

答案 3 :(得分:1)

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

对于迭代数组并在没有任何特殊技巧的情况下调用异步代码,您只需使用Array.prototype.forEach(在某些浏览器上,classList属性也支持此方法):

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

由于pforEach回调的绑定参数,它始终保持当前迭代的期望值 ,并且正确的元素切换。

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

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