由for循环变量作用域引出的一系列问题——闭包、事件委托

今天遇到一个有趣的事情,首先看下面代码

<!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

*pic 出现的问题现象*

问题的原因

首先,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. 事件冒泡阶段

  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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值