闭包在循环事件绑定中的应用
- 场景描述:
在页面上有3个button按钮,当点击每个按钮时输出当前按钮的索引值,要求用循环来绑定每个按钮的事件,看下面的代码。
<button>button1</button>
<button>button2</button>
<button>button3</button>
var buttons = document.querySelectorAll('button');
for(var i = 0; i < buttons.length; i++){
buttons[i].onclick = function(){
console.log(`当前按钮的索引是${{i}`);
}
}
- 原因剖析
上述代码执行完成,发现并不是我们想要的效果,不管点击哪个按钮输出的都是3,而不是我们期望0,/1/2。那么代码看上去并没有问题,输出结果却为什么不是我们期望的呢?
- 这是因为每次for循环都只是开辟一块堆内存创建一个函数并将函数在堆内存中的地址赋值给每个button对象的属性onclick,这里并没有执行函数。
- 每循环一次i都会自动加1,直到i=3时循环结束。这时当我们点击某个按钮时才会去执行当前按钮的onclick属性所绑定的函数
- 函数执行时会形成一个私有上下文(作用域为全局上下文),在这个私有上下文中会用到变量i,但是该上下文中并不存在变量i,于是向上级上下文(也就是全局上下文)中查找,发现有变量i,但此时的i经过三次循环后值已经变成了3(==i=3==) 所以最终不管我们点击哪个按钮输出的结果都是3.
-
一图解真相
-
解决方案:基于闭包机制实现
那么如果就是想要输出我们期望的那种结果该怎么解决呢?这时闭包就派上用场了。我们先将上面的js代码改造如下:
方案一
var buttons = document.querySelectorAll('button');
for(var i = 0; i < buttons.length; i++){
(function(i){
buttons[i].onclick = function(){
console.log(`当前按钮的索引是${{i}`);
}
})(i);
}
在原来代码的基础上给绑定事件套一个自执行函数,这样在每一轮循环时,自执行函数都会自动执行;而每个自执行函数执行时都是形成一个独立的私有上下文,在私有上下文中会把形参i作为私有变量保存,同时执行按钮的事件绑定代码,创建一个新的函数并让全局上下文中的button的onclick属性占用当前创建的函数,从而形成了闭包。 这样当我们再去点击某个按钮时,按钮绑定的对应函数执行,并到上级上下文(自执行函数所形成的上下文)中寻找变量i,而3个自执行函数的上下文中分别存储了3个不同的i的值(0/1/2)。从而就实现了我们期望的效果。
- 一图解真相
方案二
上面的代码还可以改为如下样式,同样也是基于闭包机制实现。让button的onclick属性直接等于一个自执行函数,然后将里面的函数作为返回值返回给onclick属性。
var buttons = document.querySelectorAll('button');
for(var i = 0; i < buttons.length; i++){
buttons[i].onclick = (function(i){
return function(){
console.log(`当前按钮的索引是${{i}`);
}
})(i);
}
方案三
还有一种更为简单的方法,就是将for循环中的var改为let同样也能实现相同的效果。这种方案的原理也是基于闭包实现。
var buttons = document.querySelectorAll('button');
for(let i = 0; i < buttons.length; i++){
buttons[i].onclick = function(){
console.log(`当前按钮的索引是${{i}`);
}
}
- 扩展方案一:自定义属性
上述基于闭包机制的方案虽然能够解决问题,但并不是最优方案,因为如果有很多按钮,那么就会产生很多闭包,这样对于程序的性能影响还是很大的。接下来介绍一种比闭包好一点的方案,那就是利用节点的自定义属性来实现。 在循环绑定时给每个button都加一个自定义属性例如:index,这样在每次点击按钮事件时,都从按钮自身的自定义属性获取值就可以了。
var buttons = document.querySelectorAll('button');
for(var i = 0; i < buttons.length; i++){
buttons[i].index = i;
buttons[i].onclick = function(){
console.log(`当前按钮的索引是${{this.index}`);
}
}
- 扩展方案二:事件委托
该方案利用事件委托来实现,原理就是我们先给每个button元素手动加上index索引,然后给document.body对象的onclick属性绑定一个方法(这样不管点击哪里都会触发事件),然后在方内部获取事件的事件源(也就是哪个元素触发的),再判断该事件是否有button触发,如果是则直接获取button的index属性值即可
<button index="1">button1</button>
<button index="2">button2</button>
<button index="3">button3</button>
document.body.onclick = function(ev){
var target = ev.target, targetTagName = target.tagName;
if(targetTagName === "BUTTONS"){
var index = target.getAttribute("index");
console.log(`当前按钮的索引是${{index}`;
}
}