数字营销 · Web开发 · 基础设施

Javascript中for循环为多个对象添加事件的问题

通过一个 for 循环给多个元素绑定 click 事件的例子,理解变量作用域、循环结束后的变量值以及闭包的解决方法。

前一段时间在编写一段 JS 代码时遇到了这样的问题。

在同一个父元素下面有很多相同的 li 元素,我需要遍历这些元素,并给每个 li 都添加一个 click 事件,用来显示当前 li 元素在父元素中的顺序。

代码是这样的:

<ul>
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>

<script>
var list=document.getElementsByTagName("li");

for(var i=0; i<list.length; i++){
list[i].onclick =function(){
alert(i);
};
}
</script>

执行之后会发现一个比较有意思的结果:点击不同的 li,每次弹出的结果都是 6。

这里其实有两个问题:

第一个问题是,为什么点击不同的 li,没有弹出对应的数字?

第二个问题是,为什么弹出的是 6,而不是 5 或者其他数字?

为什么每个 li 都弹出相同的数字?

先说第一个问题。

从代码可以看出来,我们通过:

list[i].onclick =function(){
alert(i);
};

给每个对象都绑定了一个事件处理函数。

这里的函数是一个没有参数的匿名函数:

function(){
alert(i);
}

我们用 for 循环把这个事件处理函数绑定到了所有的 li 上。当 for 循环执行完毕以后,事件绑定也已经完成了。之后,当我们点击某一个 li 时,才会真正执行对应的事件处理函数。问题就在这里。

这个函数内部需要访问变量 i 但是 i 并不是这个函数内部声明的变量,也不是函数的参数,所以 JavaScript 会按照作用域向外查找。最终找到了 for 循环外层的 i。也就是说我们绑定给各个 li 的函数虽然分别保存到了不同的对象上,但是这些函数访问的都是外部的同一个 i。而当鼠标真正点击 li 的时候,前面的 for 循环早已经执行完了。

因此,这时候所有事件处理函数访问到的都是循环结束之后的 i。这也是为什么 点击不同的 li,得到的却是同一个结果

为什么最终是 6?

再来看第二个问题。

为什么最后的结果是 6?

我们把 for 循环换一种方式写,就比较容易理解了:

var i=0;

while(i<list.length){
i++;
}
alert(i);

如果 list.length 是 6,那么执行过程就是:

i = 0
0 < 6 → i = 1
1 < 6 → i = 2
2 < 6 → i = 3
3 < 6 → i = 4
4 < 6 → i = 5
5 < 6 → i = 6
6 < 6 → false

所以最后:i的值就是:6而不是 5。

i < list.length 的意思是:只要 i 小于 list.length,循环就继续执行。

当 i 已经等于 5 的时候 5 < 6 条件仍然成立,所以还会再执行一次 i++;

这时候 i 就变成了 6 下一次判断 6 < 6 条件不成立,循环结束, 所以最终的 i 就是 6。

到这里,前面两个问题就都解释清楚了。

如何避免此类问题?

这个问题的解决方法其实有很多。

这里介绍一种比较通用的方法,就是利用闭包保存当前循环中的 i。

<script>

var lis=document.getElementsByTagName("li");

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

lis[i].onclick = (function(arg){
return function(){
alert(arg);
};
})(i);
}
</script>

这里通过一个立即执行函数,把当前循环中的 i 作为参数传进去:

(function(arg){
return function(){
alert(arg);
};
})(i);

每一次循环都会生成一个独立的 arg,因此最终每个 li 保存的事件处理函数,都拥有自己对应的数字。

例如:

第一个 li → arg = 0
第二个 li → arg = 1
第三个 li → arg = 2
……
第六个 li → arg = 5

所以点击的时候,就能够得到对应的结果。

闭包可能带来内存方面的一些问题,还有很多其他的解决方法,这里就不再展开了。

评论0

欢迎分享你的看法,也欢迎补充不同的实践经验。