使用 for 循环添加 addEventListener 并传递值

114

我试图使用for循环向多个对象添加事件监听器,但最终所有的监听器都会针对同一个对象进行 --> 最后一个对象。

如果我通过为每个实例定义boxa和boxb来手动添加监听器,则可以正常工作。我想这是由于addEvent的for循环并没有按照我期望的方式工作。也许我整个方法都错了。

以下是使用class="container"的4个示例。在container 4上触发的操作按预期工作。 在container 1,2,3上触发的操作会触发container 4上的事件,但前提是已经激活了触发器。

// Function to run on click:
function makeItHappen(elem, elem2) {
  var el = document.getElementById(elem);
  el.style.backgroundColor = "red";
  var el2 = document.getElementById(elem2);
  el2.style.backgroundColor = "blue";
}

// Autoloading function to add the listeners:
var elem = document.getElementsByClassName("triggerClass");

for (var i = 0; i < elem.length; i += 2) {
  var k = i + 1;
  var boxa = elem[i].parentNode.id;
  var boxb = elem[k].parentNode.id;

  elem[i].addEventListener("click", function() {
    makeItHappen(boxa, boxb);
  }, false);
  elem[k].addEventListener("click", function() {
    makeItHappen(boxb, boxa);
  }, false);
}
<div class="container">
  <div class="one" id="box1">
    <p class="triggerClass">some text</p>
  </div>
  <div class="two" id="box2">
    <p class="triggerClass">some text</p>
  </div>
</div>

<div class="container">
  <div class="one" id="box3">
    <p class="triggerClass">some text</p>
  </div>
  <div class="two" id="box4">
    <p class="triggerClass">some text</p>
  </div>
</div>


1
这是因为闭包的原因 :D ...但是为什么不将事件监听器添加到父元素上,而是要将其添加到每个子元素上呢? - Abhidev
5个回答

163
Closures! :D
这个修复后的代码按照您的意图工作:

// Function to run on click:
function makeItHappen(elem, elem2) {
    var el = document.getElementById(elem);
    el.style.backgroundColor = "red";
    var el2 = document.getElementById(elem2);
    el2.style.backgroundColor = "blue";
}

// Autoloading function to add the listeners:
var elem = document.getElementsByClassName("triggerClass");

for (var i = 0; i < elem.length; i += 2) {
    (function () {
        var k = i + 1;
        var boxa = elem[i].parentNode.id;
        var boxb = elem[k].parentNode.id;
        elem[i].addEventListener("click", function() { makeItHappen(boxa,boxb); }, false);
        elem[k].addEventListener("click", function() { makeItHappen(boxb,boxa); }, false);
    }()); // immediate invocation
}
<div class="container">
  <div class="one" id="box1">
    <p class="triggerClass">some text</p>
  </div>
  <div class="two" id="box2">
    <p class="triggerClass">some text</p>
  </div>
</div>

<div class="container">
  <div class="one" id="box3">
    <p class="triggerClass">some text</p>
  </div>
  <div class="two" id="box4">
    <p class="triggerClass">some text</p>
  </div>
</div>


为什么这个能修复它?
for(var i=0; i < elem.length; i+=2){
    var k = i + 1;
    var boxa = elem[i].parentNode.id;
    var boxb = elem[k].parentNode.id;

    elem[i].addEventListener("click", function(){makeItHappen(boxa,boxb);}, false);
    elem[k].addEventListener("click", function(){makeItHappen(boxb,boxa);}, false);
}

实际上是非严格模式的JavaScript。它被解释为:

var i, k, boxa, boxb;
for(i=0; i < elem.length; i+=2){
    k = i + 1;
    boxa = elem[i].parentNode.id;
    boxb = elem[k].parentNode.id;

    elem[i].addEventListener("click", function(){makeItHappen(boxa,boxb);}, false);
    elem[k].addEventListener("click", function(){makeItHappen(boxb,boxa);}, false);
}

由于变量提升var声明会被移动到作用域的顶部。由于JavaScript没有块级作用域(for, if, while等),它们会被移动到函数的顶部。更新:从ES6开始,您可以使用let获取块级作用域变量。
当您的代码运行时,以下内容会发生:在for循环中,您添加了点击回调并分配了boxa,但其值在下一次迭代中被覆盖。当单击事件触发时,回调运行,并且boxa的值始终是列表中的最后一个元素。
使用闭包(关闭boxaboxb等的值),将该值绑定到单击处理程序的作用域。

代码分析工具,例如JSLintJSHint,可以捕捉到类似这样的可疑代码。如果你写了很多代码,那么学习如何使用这些工具是值得的。一些集成开发环境(IDE)已经内置了这些工具。


@musefan 他们是问题所在,因为存在变量提升。请看我的解释。 - Halcyon
2
应该将 }(i, k)) 改为 })(i, k) 吗? - lastr2d2
2
实际上是无效的JavaScript代码:如果它无效,那么它将无法解析或执行。像我展示给你的那样使用闭包,你确实会得到你期望的行为:是的,但重要的是你在循环内执行一个函数,这将创建一个新的作用域。 - Felix Kling
@Wayne:两种方式都可以。个人而言,我认为第一种更易读。 - Felix Kling
@Felix 土豆土豆,将措辞更改为“非严格的”。@Wayne 两个都可以,但我更喜欢 }()) - Halcyon
显示剩余3条评论

17
你可以使用函数绑定,不需要使用闭包。请参见下面的内容:

Before:

function addEvents(){
   var elem = document.getElementsByClassName("triggerClass");

   for(var i=0; i < elem.length; i+=2){
      var k = i + 1;
      var boxa = elem[i].parentNode.id;
      var boxb = elem[k].parentNode.id;

      elem[i].addEventListener("click", function(){makeItHappen(boxa,boxb);}, false);
      elem[k].addEventListener("click", function(){makeItHappen(boxb,boxa);}, false);
   }
}

之后:

function addEvents(){
   var elem = document.getElementsByClassName("triggerClass");

   for(var i=0; i < elem.length; i+=2){
        var k = i + 1;
      var boxa = elem[i].parentNode.id;
      var boxb = elem[k].parentNode.id;
      elem[i].addEventListener("click", makeItHappen.bind(this, boxa, boxb), false);
      elem[k].addEventListener("click", makeItHappen.bind(this, boxa, boxb), false);
   }
}

我喜欢这个解决方案。它非常清晰易读。 - Magnus
我也喜欢这个。谢谢。 - Pak Ho Cheung
不错的解决方案,易于实现且有效。您能否提供更多关于它如何/为什么有效的见解?(我知道我可以搜索,但对于未来到达此处的人来说,直接在这里提供1或2段会很好。) - Pedro Gomes
1
我真的不知道这是怎么工作的,但它确实很好,并且非常干净!非常感谢! - bolkay

15

如果您遇到作用域/闭包问题,例如function(){makeItHappen(boxa,boxb);}中的boxaboxb引用,则始终是最后一个元素。

要解决此问题:

function makeItHappenDelegate(a, b) {
  return function(){
      makeItHappen(a, b)
  }
}

// ...
 elem[i].addEventListener("click", makeItHappenDelegate(boxa,boxb), false);

1
这本质上是相同的解决方案。您可以通过返回一个绑定到boxaboxb实际值的函数来在makeItHappenDelegate中创建一个闭包。 - Halcyon
可以,但如果我们不在每个循环中创建自我调用函数,则会更好。是的,我们创建了委托,但这是最小的开销。 - tenbits
在这种情况下,我怀疑(极小的)性能开销是否超过了可读性的好处。如果你有长列表,比如10万个以上,你可能会看到性能上的好处,但是在这之下,我真的不会担心它。可读性更重要。 - Halcyon
2
嗯,委托是更好的方法,甚至在可读性和可维护性方面也是如此。SOLID - tenbits
我同意,我觉得这样更易读和可维护。 - Bernd

2

这是因为闭包的原因。

看看这个链接:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures#Creating_closures_in_loops_A_common_mistake

示例代码和您的代码本质上是相同的,对于那些不了解“闭包”的人来说,这是一个常见的错误。

简单来说,当您在addEvents()中创建处理程序函数时,它不仅可以从addEvents()的环境访问变量i,而且还会“记住”i

由于处理程序“记住”i,所以变量i在执行addEvents()后不会消失。

因此,当调用处理程序时,它将使用i,但变量i现在已经是3了,即在for循环之后。


1

我之前也遇到过这个问题。我通过在循环外部使用“adds”函数来分配事件,解决了这个问题,它完美地运行了。

你的脚本应该看起来像这样。

function addEvents(){
  var elem = document.getElementsByClassName("triggerClass");
  for(var i=0; i < elem.length; i+=2){
    var k = i + 1;
    var boxa = elem[i].parentNode.id;
    var boxb = elem[k].parentNode.id;

//- edit ---------------------------|
    adds(boxa, boxb);
  }
}
//- adds function ----|
function adds(obj1, obj2){
  obj1.addEventListener("click", function(){makeItHappen(obj1, obj2);}, false);
  obj2.addEventListener("click", function(){makeItHappen(obj1, obj2);}, false);
}
//- end edit -----------------------|

function makeItHappen(elem, elem2){
  var el = document.getElementById(elem);
  el.style.transform = "flip it";
  var el2 = document.getElementById(elem2);
  el2.style.transform = "flip it";
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接