for 循环中为何出现 Cannot read properties of undefined 错误?
报错是因为所有事件处理函数共享同一个 var i。鼠标移入图片时,循环已经结束,i 等于 heads.length,此时 heads[i] 为 undefined,访问它的 .style 就会报错。
最小的改动是把 var i 改成 let i。也可以在事件处理函数中使用 event.currentTarget,直接获取绑定监听器的元素。
为什么绑定成功,触发事件时却报错?
调用下面的 addEventListener() 时,循环还在进行,i 是有效索引,所以能正常添加监听器:
heads[i].addEventListener("mouseover", function () {
heads[i].style.animation = "colorIn 2s";
});
传入的回调函数要等事件触发后才执行,其中的 heads[i] 也要到那时才求值。注册监听器时,回调并没有保存对应的图片。
用 var 声明的 i 属于 main() 的函数作用域,每轮循环创建的回调都通过闭包访问同一个 i。题目中有 8 张图片,假设集合没有变化:
- 注册监听器时,
i依次为0到7。 - 循环结束时,
i为8。 - 鼠标移入任意图片后,回调访问的都是
heads[8].style。
集合最后一个有效索引是 7,因此 heads[8] 返回 undefined。错误信息中的 reading 'style' 就是指这次属性访问。
方法一:把 var 改为 let
在 for 循环中,用 let 声明的变量在每轮迭代中都有独立的绑定,回调因此能保留各自那一轮的索引。
将 main.js 替换为:
function main() {
const heads = document.getElementsByClassName("heads");
for (let i = 0; i < heads.length; i++) {
heads[i].addEventListener("mouseover", function () {
heads[i].style.animation = "colorIn 2s";
heads[i].style.filter = "none";
});
}
}
原来的 <body onload="main()"> 可以继续使用。对于题目中这些固定的图片,这样就能解决报错。
方法二:通过 event.currentTarget 获取对应元素
如果回调只需要修改绑定监听器的图片,可以直接使用 event.currentTarget,触发事件时就不必再通过索引查找元素:
function main() {
const heads = document.getElementsByClassName("heads");
for (const head of heads) {
head.addEventListener("mouseover", function (event) {
const element = event.currentTarget;
element.style.animation = "colorIn 2s";
element.style.filter = "none";
});
}
}
浏览器调用监听函数时,会把事件对象作为参数传入。所以,function (event) 接收的是事件对象,即使把参数名改成 head 或 element,浏览器传入的也仍然是事件对象。
event.currentTarget 是当前正在执行的监听器所绑定的元素。这里的监听器直接绑定在每张图片上,因此它就是要修改的图片。
event.target 是事件最初发生的目标。如果以后把监听器绑定到包含子元素的容器上,两者可能不同。要修改监听器绑定元素的样式,使用 currentTarget 更直接。
注意事项
- 原代码中的
heads = ...没有声明变量。在非严格模式下,这可能意外创建全局变量;在严格模式下则会抛出ReferenceError。应使用const heads = ...。 getElementsByClassName()返回动态的HTMLCollection。之后如果增删元素或修改相关类名,集合内容和索引可能发生变化。let能解决循环变量共享的问题;使用event.currentTarget则可以直接访问绑定元素,避免依赖事件触发时集合中的索引。- 在传给
addEventListener()的普通函数监听器中,也可以用this.style访问绑定元素。箭头函数没有自己的this,不能直接照搬这种写法。 - 这个异常发生在读取
.style时,修改 CSS 无法解决索引越界问题。重复设置相同的animation值通常也不会重新播放动画。如果需要每次鼠标移入都重播,还要另行处理动画重置。
备注:内容仅供参考。
