今天遇到一个有趣的事情,首先看下面代码
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="testStyle.css">
</head>
<body>
<div class="box" id="box">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
</body>
<script>
var boxCh = document.getElementById("box").children;
for (var i = 0, len = boxCh.length; i < len; i++) {
boxCh[i].onclick = function(){
console.log(boxCh[i]);
}
}
</script>
</html>
CSS的不重要,我就压缩一下,贴在下面,想试试效果的朋友拿走就好
.box div{margin:10px 10px;width:50px;height:50px;border-radius:50%;background-color:#ddd;float:left;text-align:center;line-height:50px;}
.box .active{background-color:#f0f;}
出现的问题
我想要的效果是:点击每一个 div
,就打印出这个元素,可是意外的是,打印出的都是 undefined
。
问题的原因
首先,JS 中没有 块级作用域
,这里 for
循环中i
的作用域是全局作用域
。在JS中,事件绑定的函数只有在事件被触发后才会执行,而在这段代码中,for
循环中的函数还没有来得及被执行过(除非手速极快,可以在页面刚渲染完成就点击元素),变量 i
就已经遍历到了 4,这个 4 就被保存到了全局变量 i
中。当触发事件时,for
循环不会再去遍历一次,而是把 4 给了这个事件。
所以在上面的例子中,打印出的永远是不存在的元素,只能是显示 undefined
。
解决办法
办法一 —— 使用let
for (let i = 0, len = boxCh.length; i < len; i++) {}
let
在 ES6 中相当常见,let
可以在每次循环时声明遍历 i
,当前的变量只在本轮循环中有效,相当于块级作用域,实际上,每轮循环的i
都是一个新的变量。但是由于 IE 的兼容性问题,这个用法在实际中不算常见。
办法二 —— 使用闭包
我们知道,闭包可以延长一个函数的作用域链,或者说扩展了一个函数的作用域。使用闭包,我们可以像下面这样写
var boxCh = document.getElementById("box").children;
for (var i = 0, len = boxCh.length; i < len; i++) {
boxCh[i].onclick = (function(i){
return function(){
console.log(boxCh[i]);
}
})(i);
}
或者这样写
var boxCh = document.getElementById("box").children;
for (var i = 0, len = boxCh.length; i < len; i++) {
(function(i){
boxCh[i].onclick = function(){
console.log(boxCh[i]);
}
})(i);
}
这两个方法都是通过一个立即执行函数包裹另一个函数,实现闭包。i
会作为参数给立即执行函数,这个函数声明之后就会立即执行,这样就能保存 i
的值,在触发 click
事件的时候便能实现想要的效果。上面两种方法使用何种都是差不多的。
但是,由于多了一个函数,并且延申了内部函数的作用域链,闭包对性能是有一定的影响的。而且,我们使用 for
循环来遍历,执行DOM
操作,本身就是一个不够明智的选择,而 JS 为我们提供了一个专门为操作 DOM
的方法—— 事件委托
办法三 —— 使用事件委托
首先看一下代码
var list = document.getElementById("box");
list.addEventListener("click", function (event) {
var event = event || window.event;
var target = event.target || event.srcElement;
if (target.nodeName.toLowerCase() == "div") {
console.log(target);
}
})
首先获取了<div>
的父级元素,然后对其绑定了 click
事件监听,执行回调函数,这个函数传入了一个叫 event
的参数,并赋值给 event
变量,这里使用了一个兼容的写法,IE 中为 window.event
。之后声明了一个 target
变量,同样是一个兼容性的写法,这个 target
指向事件的目标元素。之后使用 if
判断,如果这个元素的节点名称的小写是 “div”
,那就打印出这个元素。
但是这个 click
事件是绑定在被点击元素的父级元素上的,能行吗?我们运行一下
看来能行的,在浏览器中,DOM
事件的处理是分为三步的:1. 事件捕获阶段、2. 事件目标阶段、3. 事件冒泡阶段
事件捕获阶段
事件捕获的思想是不太具体的节点应该更早接收到事件,而更具体的节点应该最后接收到事件。事件捕获的用意在于事件到达预定目标之前捕获它
事件目标阶段
事件冒泡阶段
事件冒泡,即事件开始时由最具体的元素(文档中嵌套最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)
引自《JavaScript高级程序设计》(第三版)
运用到我们这个例子,首先发送事件捕获,好让浏览器可以截获事件。然后将实际的目标元素 div
接收到事件中。最后发生事件冒泡,而事件目标阶段可以看作是事件冒泡的第一部分,之后,事件向顶层传播,首先经过 div#box
,最后传播回 document
。
可以看到,这个 click
事件终究会经过那些被点击元素的父元素 div#box
,所以我们的写法是没有毛病的。
而 event
对象是为 JS事件 “定制”的,其中保存着特别多的信息,而这里我们使用到的就是 event.target
,它指向目标元素,在这里就是被点击的某个 div
元素。
使用事件委托的好处是,只需要一个函数,就能给众多元素绑定 DOM
事件。要知道,函数是对象,而对象就必须给分配内存。所以使用事件委托可以减少很多 DOM
操作的函数。事实上,这个事件可以直接绑定给 document
,因为事件捕获从document
开始,事件冒泡最终会回到 document
,绑定在 document
不但可以减少函数,而且还能提升事件处理的速度,但是,这样写,在进行节点判断的时候可能需要多下点功夫,不能只是判断是否是 div
元素这么简单,更多的可能是判断 id
之类的。
可以使用事件委托的事件类型有 click,mousedown,mouseup,keydown,keyup,keypress等。具体有关事件委托,可以看看下面这篇文章:js中的事件委托或事件代理详解
参考链接
https://blog.youkuaiyun.com/Gushiyuta/article/details/92433503
https://blog.youkuaiyun.com/u014182411/article/details/74452536