一、前言
在 DOM 节点中,或者在循环引用中,如何点击每个兄弟节点获取对应节点下标,比如 ul 下有 3个 li,要求实现点击每个 li 获取其对应的下标。这是一道面试题,也是项目中非常常见的功能,我在写上一篇《详解 JavaScript 中的闭包》的时候,有提到过这几个方法,但是没有详细说明,今天就给大家说说每个方法的实现原理。
二、示例
- HTML 代码示例:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
- 可能出现的错误脚本:
<script>
var lis=document.getElementsByTagName('li');
for(var i=0;i<=lis.length-1;i++){
lis[i].onclick = function () {
console.log(i)
};
}
</script>
- 出现的现象:
不管点击那个 li 都会打印出 3。 - 原因:
这是因为 for 循环中的作用域是对全局开放的,即在外部也可以访问或改变循环内的变量,以上脚本中,在全局环境下打印出 console.log(i),输出 3。而 onclick 是异步事件,即循环执行完,或者同步代码执行完,当触发点击的时候才执行该事件。请看以下脚本。
<script>
var lis=document.getElementsByTagName('li');
for(var i=0;i<=lis.length-1;i++){
lis[i].onclick = function () {
console.log(i)
};
};
i = i+3;
console.log(i);//6 点击每个 li 打印出来的就是 6 。
//以上的执行顺序等同于
var i = 0;
i++;//执行了3遍;i =3;
i = i+3; //i =6;
lis[i].onclick = function () {
console.log(i)// 6 因为该事件是异步函数,所以最后执行,此时 i 已经为6。
};
</script>
- 解决思路
所以由以上分析得出,要解决该问题,就需要用到一个外部环境访问不到的变量。
三、解决方案
1. 使用 let
//将以上循环脚本中的 var 改成 let 即可
for(let i=0;i<=lis.length-1;i++){
lis[i].onclick = function () {
console.log(i)
};
}
解析:let 是 ES6 引进的新语法,之所以可以解决这个问题是因为 let 有块级作用域。想了解更多 let 特性,可查看《ES6 语法之 let 与 const》,块级作用域的特性使 let 将循环内的变量锁定了,外层无法访问,也无法改变。
2. 给 DOM 属性赋值
for(var i=0;i<=lis.length-1;i++){
lis[i].id = i;
lis[i].onclick = function () {
console.log(this.id)
};
}
解析:这个很好理解,for 循环在执行的时候,就将变化的变量 i 赋值给每个 li 对象的 id 属性(赋值后 li 对象就保存了这个属性),通过点击获取 this 对象(当前 li)的 id 属性,即可变相的获取到下标值。
3. 使用传参闭包1
for(var i=0;i<=lis.length-1;i++){
lis[i].onclick = (function (m) {
return function(){
console.log(m)
}
})(i);
}
解析:赋值给 onclick 的事件是一个自执行的匿名函数,将循环变量 i 作为参数传递给匿名函数调用并保存起来,因为外部获取不到也改变不了形参 m ,所以当事件触发的时候获取的就是对应的下标。
4. 使用传参闭包2
for(var i=0;i<=lis.length-1;i++){
(function(m){
lis[i].onclick = function () {
console.log(m)
};
})(i)
}
解析:在循环体内再创建一个函数,使用该函数的参数与循环变量当前的值绑定,无论该循环变量如何改变, 已绑定到该函数参数的值不会被外部改变。
5. 使用不传参闭包+变量关联
for(var i=0;i<=lis.length-1;i++){
(function(){
var id = i;
lis[i].onclick = function () {
console.log(id)
};
})()
}
解析:创建一个匿名函数自执行的词法作用域,把当前循环项内的 id 与事件回调绑定起来,并保存对应的 id 值,在打印的时候打印 id 值即可查找到对应的下标值。
6. 使用数组循坏
var lis = document.getElementsByTagName('li');
var item = Array.from(lis);
item.map((item,i)=>{
item.onclick=function () {
console.log(i);
}})
解析:使用 Array将集合转换为数组形式,再用 map 循坏,因为 map 循坏中都是局部变量,外部访问不到,所以不会被修改或受影响。
四、总结
其实看着很复杂,思路很简单,只要保证这个下标变量不被外部环境所影响即可。换句话说,它要有自己独立的局部作用域。而 for 循坏内不是块级作用域,其内部的变量会被外部所影响。