var 产生的闭包问题

2020-04-29  本文已影响0人  BA_凌晨四点

曾经见过这样的一道面试题:

 <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);
    })
}

结果。。

图片.png
为何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);
    })
}

但是利用这种原理去解决,都是要写一堆很恶心的代码。。。就为了这个小问题。

我们来分析一下。

图片.png
所以,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

上一篇下一篇

猜你喜欢

热点阅读