这段js循环代码addEventListener为什么会错误?

// 这个代码是错误的,因为变量i从来就没被locked住
// 相反,当循环执行以后,我们在点击的时候i才获得数值
// 因为这个时候i操真正获得值
// 所以说无论点击那个连接,最终显示的都是I am link #10(如果有10个a元素的话)

var elems = document.getElementsByTagName('a');

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

    elems[i].addEventListener('click', function (e) {
        e.preventDefault();
        alert('I am link #' + i);
    }, 'false');

};

它为什么没有在点击第i个链接时,显示I am link #i

阅读 11.2k
3 个回答

这是典型的 js 闭包问题。

学习这个东西最好的办法,就是设置断点用调试器debug一下,比看书和别人的文章要简单多。

事件绑定和闭包

先说明下js的事件绑定或者叫事件监听。比如下面代码:

var i = 10;

a.onclick = function(){
    alert('this is ' + i)   // this is 10
}

a对象绑定了click事件click事件函数是弹出一个i的值,i在匿名函数内,没有这个变量,可是匿名函数外面的全局有这个i,所以最后会弹出10.这里面的过程是,堆内存定义一个匿名函数,然后栈里面有一个a对象的变量,这个变量对象又绑定到匿名函数。

例子解释

这里模仿解释器的执行过程来解释。
题主的代码运行的时候,循环一个elems数组,每个数组绑定一个事件函数。内存大概如下


elem[0].addEventListener('click', function (e) { ... alert('I am link #' + i); // 注意 这个 i 不是 循环变量。 }, 'false'); elem[1].addEventListener('click', function (e) { ... alert('I am link #' + i); }, 'false'); ...

为什么不是i不是循环变量?很简单,解释器读取循环的代码的时候,在栈内存生成了一些变量,比如elem[0]``elem[1]...,堆定义了一个匿名函数,这个匿名函数里面写的是什么,就是什么。比如这里匿名函数的函数内容是 alert('I am link #' + i); 每一个栈里的变量对象都绑定了堆里面那个匿名函数(事件函数).就是上面代码描述的样子。

贴个简图示意一下:
请输入图片描述

然后点击对象,触发点击事件的时候,就和前面事件绑定与闭包的例子一样了。因为循环结束后,全局还存在一个i的变量,并且它的值是循环之后的值,也就是10.点击的时候就执行这个匿名函数。

解决方法

因为i相对匿名函数是外面的变量,就把循环绑定的时候,将i的值传入到匿名函数内,就可以了。因此需要在匿名函数(事件函数)外包裹一个匿名函数, 并立即执行


var elems = document.getElementsByTagName('a'); for (var i = 0; i < elems.length; i++) { elems[i].addEventListener('click', (function (num) { return function (e){ e.preventDefault(); alert('I am link #' + num); } })(i), 'false'); };

如果执行点击事件的时候,最终的事件函数外层因为立即执行的匿名函数,函数体内已经存在了num变量,而这个num变量是每次循环的时候传入的i

另外一种解决方法没有用到闭包,而是给每个对象添加一个属性

var elems = document.getElementsByTagName('a');
for (var i = 0; i < elems.length; i++) {
    elems[i].num = i;
    elems[i].addEventListener('click', function (e) {
        e.preventDefault();
        alert('I am link #' + this.num);
    }, 'false');

};

刚好我也在解决闭包问题

var ps=document.getElementsByTagName("p");
for (var i = 0; i < ps.length; i++) {

ps[i].addEventListener("click",(function(num){
    return function(){
       alert(num);
    };
})(num));

}
刚测试出来的。

把for循环中的var改成let

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏