闭包在项目中的应用

90 阅读1分钟

老生常谈的闭包,你知道闭包的真正应用场景吗?

循环事件绑定

我们有五个按钮,想要点击的时候,打印按钮的下标记。

<!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++;
}
  1. 首先,for 循环会产生一个父级的上下文,用于声明 i 初始值,做循环条件判断运算。
  2. 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>