老生常谈的闭包,你知道闭包的真正应用场景吗?
循环事件绑定
我们有五个按钮,想要点击的时候,打印按钮的下标记。
<!DOCTYPE html>
<html lang="en">
<body>
<span>测试</span>
<button class="btn">0</button>
<button class="btn">1</button>
<button class="btn">2</button>
<button class="btn">3</button>
<button class="btn">4</button>
</body>
<script type="text/javascript">
var btnList = document.querySelectorAll('.btn');
for (var i = 0; i < btnList.length; i++) {
btnList[i].onclick = function() {
console.log(`当前点击按钮的索引: ${ i }`); // 5
}
}
console.log(i); // 5
</script>
</html>
我们看到,会输出五次 5
其实每次执行对应的方法时,方法中的 i 不是私有的,而是全局的,而此时全局的 i 已经是循环结束的 5 了, 下图为执行过程。
注意 var 不会受限于块级上下文,所以 var i,相当于在全局上下文中声明变量 i
方案1:闭包
利用闭包的"保存"机制
每一轮循环的时候,都创建一个闭包(不释放的上下文),利用一个匿名函数储存当前的 i
OK,两种常规写法:
for (var i = 0; i < btnList.length; i++) {
// 循环五次,产生五个不释放的闭包,每一个闭包中,都存在一个私有变量 i
(function(i) {
btnList[i].onclick = function() {
console.log(`当前点击按钮的索引: ${ i }`);
}
})(i);
}
for (var i = 0; i < btnList.length; i++) {
// 循环五次,产生五个不释放的闭包,每一个闭包中,都存在一个私有变量 i
btnList[i].onclick = (function(i) {
return function() {
console.log(`当前点击按钮的索引: ${ i }`);
}
})(i)
}
闭包之 forEach 利用 forEach 每轮执行一次匿名函数的规则,使用每次的匿名函数保存索引 idx。
btnList.forEach((itm, idx) => {
itm.onclick = function() {
console.log(`当前点击按钮的索引: ${ idx }`);
}
});
闭包之 let 也是基于闭包的方案,只不过利用的是 let 会产生块级上下文
for (let i = 0; i < btnList.length; i++) {
btnList[i].onclick = function() {
console.log(`当前点击按钮的索引: ${ i }`);
}
}
为什么 let 也能算作闭包呢,我们看下面这段代码
for (let i = 0; i < 5; i++) {
console.log(i); // 0 2 4
i++;
}
- 首先,for 循环会产生一个父级的上下文,用于声明 i 初始值,做循环条件判断运算。
- let 声明的初始值(必须在循环条件内部包含 let),每一轮会产生一个新的块级上下文,会先把父上下文中的 i 同步过来,操作完成后还会把结果返回给父级上下文。
所以
for (let i = 0; i < btnList.length; i++) {
// let 声明的初始值,每一轮循环都会产生块级上下文
// 块级上下文中有一个私有变量 i,这个 i 同步至父级上下文(for 循环)
// 所以相当于每个块级上下文中保存了当前的 i 值
btnList[i].onclick = function() {
console.log(`当前点击按钮的索引: ${ i }`);
}
}
闭包执行栈不会释放,比如 100 个按钮,我们会产生 100 个闭包,对内存也是一种损耗,那么不用闭包我们能解决么?
方案2:自定义属性
思路:每一轮循环给当前 dom 节点添加一个自定义属性(保存到每个 dom 对象的堆内存),保存索引。
for (var i = 0; i < btnList.length; i++) {
btnList[i].myIndex = i;
btnList[i].onclick = function() {
console.log(`当前点击按钮的索引: ${ this.myIndex }`);
}
}
性能比闭包要好一些,但是也有一些性能损耗(优化了闭包式栈内存的消耗,但是增加了堆内存的消耗),那有没有更好的方案呢。
终极方案:事件捕获
<!-- 给每个按钮增加一个属性 data-index -->
<!DOCTYPE html>
<html lang="en">
<body>
<span>测试</span>
<button class="btn" data-index="0">0</button>
<button class="btn" data-index="1">1</button>
<button class="btn" data-index="2">2</button>
<button class="btn" data-index="3">3</button>
<button class="btn" data-index="4">4</button>
</body>
<script type="text/javascript">
// 点击每一个按钮,除了触发按钮的点击事件行为
// 根据冒泡传播机制,也会把 body 的点击事件行为触发
document.body.onclick = function(event) {
let target = event.target;
// 注意大写
if (target.tagName == 'BUTTON' && target.className == 'btn') {
let index = target.getAttribute('data-index');
console.log(`当前点击按钮的索引:${ index }`);
}
}
</script>
</html>