var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
console.log(i)
// 此时的 i 是声明在for循环内的,且{ }中是独立的块级作用域, 它将 i 重新声明在了for循环的**每一次**迭代中。
// 此处console.log(i) => 未定义。
//所以此处执行a[6]函数,打印的i是当时声明在当时层的块级作用域中的 let i =6。
a[6](); // 6
使用let声明的变量在块级作用域内能强制执行更新变量,下面的两个例子对比:
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {console.log(i);};
a[0](); // 10
a[1](); // 10
a[6](); // 10
/********************/
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {console.log(i);};
a[0](); // 0
a[1](); // 1
a[6](); // 6
```js
for
循环头部的let i
不仅为循环本身声明了一个i
,而是为循环的每一次迭代都更新声明了一个新的i
。这意味着loop
迭代内部创建的闭包封闭是每次迭代中的变量,也正如我们的期望一样。
而var
在外层作用域中只有一个i
,这个i
被封闭进去,而不是每次迭代都会有一个新的i
可以看到当我们在 for
循环中用 var
声明变量的时候,内部函数得到的值是循环结束后的值,而不是本次循环的值。
这是因为用 var
声明的变量没有块级作用域,所以此时 i
属于 for
循环所在作用域中的变量,所以当我们执行函数的时候,每个函数都指向了同一个变量,而此时的变量 i
因为已经结束了循环,变成了 10
,所以执行函数得到的结果是 10
。
let 做了什么
for
循环头部使用 let
声明变量 i
不止为 for
循环本身声明了一个 i
,而是为循环的每一次迭代都重新声明了一个新的 i
。所以循环内部的函数闭包封闭的是每次迭代中的变量,就像我们期望的那样。
循环陷阱指的是用var声明循环变量:
for(var i = 0; i < 10; i++) { setTimeout(()=> { console.log(i); }, 0); }
这里会输出什么呢?
10次定时器输出的10;显然不是我们想要的结果,我们想这个定时器从0到9记录输出;
为什么会这样呢?
这里用var定义循环变量i,i会被提升为全局变量,而setTimeout回调函数形成了个闭包环境引用了i,setTimeout回调执行需要等主线程的循环任务结束才会开始执行;
最后一个循环得到了9,执行i++与判断条件相比不符合,结束循环,此时i=10了,意味着全局变量i等于10,所以setTimeout输出了10个10;
为什么用let能解决呢?
for(let i = 0; i < 10; i++) { setTimeout(()=> { console.log(i); }, 0); }
首先let定义循环变量,每次循环都会产生一个块级作用域
Javascript引擎会记住上一次for循环的值来初始化本次循环变量,
每次创建的闭包定时器都会引用对应循环的变量i,所以最后输出的结果是从0到9的输出值;
总结一下:
利用let声明的循环变量在每次循环都会创建一个独立的块级作用域,在循环内部创建的闭包环境每次应用的都是对应循环变量的值,不会被变量提升指全局,每次闭包环境中引用的就是对应块级作用域中的变量,所以说let能够解决循环陷阱
在执行上下文中,扫描变量阶段
整个函数执行上下文中var 申明的变量,都会被放到了变量环境里,
而函数执行上下文中最外层的块内的let 申明的变量,被放到了词法环境栈里,词法环境只会存放当前块内的let申明的变量以及上层块内申明的变量。随着代码的执行,进入内部块后,会将内部块内let申明的变量推入词法环境栈顶,以此类推。
执行代码的代码的时候,当遇到变量,会先在当前词法环境内自顶向下寻找变量,如果没找到,再去变量环境寻找,当跳出当前块后,词法环境栈顶的所有变量弹出。这也就导致了let和const只会在所属词法环境对应的块级作用域内才有效。
这也就是let和const在整个代码编译和执行过程中的支持块级作用域的原理。
在ES5标准中,for循环都是通过var来声明变量,而在js中,var是没有独立的作用域的,变量的访问会从当前作用域开始,顺着作用域链向上查找,因此,使用var声明在循环中的作用域实际上是共用的,var所定义的变量i也是共用的,因此当在循环中使用i绑定事件,其在执行时,for循环早就执行完了,变量i也多次被赋值。
使用let可以解决循环陷阱,是因为let将变量绑定到当前所在的作用域中,形成一个块级作用域,每一次进入for循环都会有一个新的块级作用域,在事件响应函数执行时要访问变量i,会从它所在的块级作用域查找,此时的i相当于是被重新定义的。
循环陷阱问题通常可以使用闭包的方式去模拟块级作用域解决,let使得这一问题的解决方案变得更简单。
如果使用var声明变量进行for循环会有问题,是因为js没有块级作用域,只有函数作用域。
所以变量i是公用的,导致变量被多次赋值,输出的结果并不是我们所期望的。
let能解决这个问题是因为,es6中使用let和const实现了块级作用域。
每次循环都会创建一个独立的块级作用域,在循环内部创建的闭包环境每次应用的都是对应循环变量的值,不会被变量提升指全局,每次闭包环境中引用的就是对应块级作用域中的变量,所以说let能够解决循环陷阱。
大家都在说let是块作用域,每次循环都重新定义,我有一个疑问,for循环头中的let只在第一次循环开始前定义,后续每次都不会重新定义,这个“每次循环都重新定义”是如何得出的呢?
update:
如果循环头中的let每次是重新定义,那么将let替换为const应该是不会报错的,但似乎这个代码会抛异常
var a = []
for (const i=0; i<10; i++)
a[i] = function(){ console.log(i) }