var 产生的闭包问题
曾经见过这样的一道面试题:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
鼠标点击相应的<li>,输出那个<li>的索引号。
当时年少无知,这不简单吗,直接注册事件不就完事了吗?
var lis = document.querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
lis[i].addEventListener('click', function () {
console.log(i);
})
}
结果。。
为何4个4。?
后来研究了下,这是因为JavaScript的缺陷导致,也就是变量声明提升导致的闭包问题。
console.log(i); //undefined
var i = 123;
看到没有,我没有定义 i 的时候,直接打印不会报错。这是因为下面用 var 定义了 i,飘到上面去了。。这称之为函数声明提升,提升到作用域的顶端。
在lis[i]
上注册的那个click匿名函数形成了封闭的作用域(闭包)。当 for 循环结束时候,这个闭包的才打开,世界早已经发生了翻天覆地的变化了。此时的 i 不再是当年的 i 了。此时的 i 已经是4了。
解铃还须系铃人,闭包问题当然可以用闭包的方法去解决啦。
利用立即执行函数的方法:
for (var i = 0; i < lis.length; i++) {
(function (j) {
lis[j].addEventListener('click', function () {
console.log(j);
})
}(i)) //将每次循环的 i 传给 j ,保存起来。当每次执行 click 的时候,就能打印相应的索引了
}
当然也有其他的写法,但是原理是一样的
for (var i = 0; i < lis.length; i++) {
temp(i); //每次循环,及时把 i 传过去
}
function temp(j) {
lis[j].addEventListener('click', function () {
console.log(j);
})
}
但是利用这种原理去解决,都是要写一堆很恶心的代码。。。就为了这个小问题。
我们来分析一下。
所以,ES6提出了一种定义变量的方法就是 let 。它解决了原来js几个缺陷。其中就是,函数声明,不会提升,可以这么理解。
console.log(i); //报错。
let i = 123;
在循环中,用 let 声明的循环变量,会特殊处理,每次进入循环体,都会开启一个新的作用域,并且将循环变量绑定到该作用域。也就是每次进入循环,用的都是一个新的变量 i
这是因为 let 会产生一个块级作用域 {},
所以主题中说的问题可以这样写:
for (let i = 0; i < lis.length; i++) { //这里一共分别产生了4个作用域,每个域绑定1个i
lis[i].addEventListener('click', function () { //这里的i都是全新的
console.log(i);
})
}
在循环中用 let 声明的循环变量,在循环结束的时候回销毁。
因此,在循环结束的时候:
console.log(i); //error: i is not defined
其实底层实现上,let 声明的变量实际上也会有提升,但是,提升后会将其放入到“暂时性死区”,如果访问的变量位于暂时性死区(TDZ),则会报错:“Cannot access 'a' before initialization”。
只有当代码运行到该变量的声明语句时,会将其从暂时性死区中移除。
图片.png
因此,将用 let
声明的变量理解成不会提升,完全不会出问题。
而且,var
还导致了其他问题,比如:
- 全局变量挂载到全局:全局对象成员的污染问题。
- 重复声明变量, 导致覆盖。
所以,在声明变量的时候,一般不用var
去声明,尽量使用ES6的let
或者const