for循环中let与var的区别,块级作⽤域如何产⽣与迭代中变量i如
何记忆上⼀步的猜想
我在前⼀篇讨论let与var区别的博客中,顺带⼀笔带过了let与var在for循环中的不同表现,虽然解释了是
块级作⽤域的影响,但具体是怎么去影响的呢,我尝试的去理解了下,这篇博客主要从for循环步骤拆分的⾓度去理解两者的区别。
⼀、⼀个简单的for循环问题与我思考后产⽣的问题
还是这段代码,分别⽤var与let去声明变量,得到的却是完全不同的结果,为什么?如果让你把这个东西清晰的讲给别⼈听,怎么去描述呢?
//使⽤var声明,得到3个3
var a = [];
for (var i = 0; i < 3; i++) {
a[i] = function () {
console.log(i);
};
}
a[0](); //3
a[1](); //3
a[2](); //3
//使⽤let声明,得到0,1,2
var a = [];
for (let i = 0; i < 3; i++) {
a[i] = function () {
console.log(i);
};
}
a[0](); //0
a[1](); //1
a[2](); //2
再弄懂这个问题前,我们得知道for循环是怎么执⾏的。⾸先,对于⼀个for循环,设置循环变量的地⽅是⼀个⽗作⽤域,⽽循环体代码在⼀个⼦作⽤域内;别忘了for循环还有条件判断,与循环变量的⾃增。
for循序的执⾏顺序是这样的:设置循环变量(var i = 0)  ==》循环判断(i<3)  ==》满⾜执⾏循环体==》循环变量⾃增(i++)
我们按照这个逻辑改写上⾯的for循环,以第⼀个var声明为例,结合⽗⼦作⽤域的特点,上⾯的代码可以理解为:
{
//我是⽗作⽤域
var i = 0;
if (0 < 3) {
a[0] = function () {
//我是⼦作⽤域
console.log(i);
};
};
i++; //为1
if (1 < 3) {
a[1] = function () {
console.log(i);
};
};
i++; //为2
if (2 < 3) {
a[2] = function () {
console.log(i);
};
};
i++; //为3
// 跳出循环
}
//调⽤N次指向都是最终的3
a[0](); //3
a[1](); //3
a[2](); //3
那么我们此时模拟的步骤代码中的声明⽅式var修改为let,执⾏代码,发现输出的还是3个3!WTF???
按照模糊的理解,当for循环使⽤let时产⽣了块级作⽤域,每次循环块级作⽤域中的 i 都相互独⽴,并不像var那样全程共⽤了⼀个。
但是有个问题,⼦作⽤域中并没有let,何来的块级作⽤域,整个循环也就⽗作⽤域中使⽤了⼀次let i = 0;⼦作⽤域哪⾥来的块级作⽤域?
请教了下百度的同学,谈到了会不会是循环变量不⽌声明了⼀次,其实⾃⼰也考虑到了这个问题,for循环会不会因为使⽤let⽽改变了我们常规理解的执⾏顺序,⾃⼰⼜在⼦作⽤域⽤了let从⽽创造了块级作⽤域?抱着侥幸的⼼理还是打断点测试了⼀下:
可以看到,使⽤let还是⼀样,声明只有⼀次,之后就在后三个步骤中来回跳动了。
⼆、⼀个额外问题的暗⽰
如果说,在使⽤let的情况下产⽣了块级作⽤域,每次循环的i都是独⽴的⼀份,并不共⽤,那有个问题,第⼆次循环i++⾃增时⼜是怎么知道上⼀个块级作⽤域中的 i 是多少的。这⾥得到的解释是从阮⼀峰ES6⼊门获取的。
JavaScript 引擎内部会记住上⼀轮循环的值,初始化本轮的变量i时,就在上⼀轮循环的基础上进⾏计算。
那这就是JS引擎底层实现的问题了,我还真没法⽤⾃⼰的代码去模拟去实现,我们分别截图var与let断点情况下作⽤域的分布。
⾸先是var声明时,当函数执⾏时,只能在全局作⽤域中到已被修改的变量i,此时已被修改为3
⽽当我们使⽤let声明时,⼦作⽤域本没有使⽤let,不应该是是块级作⽤域,但断点显⽰却是⼀个block作⽤域,⽽且可以确定的是整个for循环let i只声明了⼀次,但产⽣了三个块级作⽤域,每个作⽤域中的 i 均不相同。
那在⼦作⽤域中,我并没有使⽤let,这个块级作⽤域哪⾥开的,从JS引擎记录 i 的变换进⾏循环⾃增⽽我们却⽆法感知⼀样,我猜测,JS引擎在let的情况下,每次循环⾃⼰都创建了⼀个块级作⽤域并塞到了for循环⾥(毕竟⼦作⽤域⾥没⽤let),所以才有了三次循环三个独⽴的块级作⽤域以及三个独⽴的 i。
这也只是我的猜测了,可能不对,如果有⼈能从JS底层实现给我解释就更好了。
PS:2019.4.19更新
之前对于for循环中使⽤let的步骤拆分推断,我们得到了两个结论以及⼀个猜想:
结论1:在for循环中使⽤let的情况下,由于块级作⽤域的影响,导致每次迭代过程中的 i 都是独⽴的存在。
结论2:既然说每次迭代的i都是独⽴的存在,那i⾃增⼜是怎么知道上次迭代的i是多少?这⾥通过ES6提到的,我们知道是js引擎底层进⾏了记忆。
猜测1:由于整个for循环的执⾏体中并没有使⽤let,但是执⾏中每次都产⽣了块级作⽤域,我猜想是由底层代码创建并塞给for执⾏体中。
由于写这篇博客的时候顺便给同事讲了let相关知识,同事今天也正好看了模拟底层实现的代码,这个做个补充:
还是上⾯的例⼦,我们在let情况下对for循环步骤拆分,代码如下:
var a = []; {
//我是⽗作⽤域
let i = 0;
if (i < 3) {
//这⼀步模拟底层实现
let k = i;
a[k] = function () {
//我是⼦作⽤域
console.log(k);
};
};
i++; //为1
if (i < 3) {
let k = i;
a[k] = function () {
console.log(k);
};
};
i++; //为2
if (i < 3) {
let k = i;
a[k] = function () {
console.log(k);
};
};
i++; //为3
/
/ 跳出循环
}
a[0](); //0
a[1](); //1
a[2](); //2
上述代码中,每次迭代新增了let k = i这⼀步,且这⼀步由底层代码实现,我们看不到;
这⼀⾏代码起到两个作⽤,第⼀是产⽣了块级作⽤域,解释了这个块级作⽤域是怎么来的,由于块级的作⽤,导致3个k互不影响。第⼆是通过赋值的⾏为让3个k都访问外部作⽤域的i,让三个k建⽴了联系,这也解释了⾃增时怎么知道上⼀步是多少。
这篇⽂章有点钻⽜⾓尖了,不过有个问题在⼼头不解决是真的难受,⼤概如此了。
PS:2019.11.28更新
谢谢博友 coltfoal 在基本数据类型与引⽤数据的概念上提供了⼀个有趣的例⼦,代码如下,猜猜输出
什么:
var a = []
for (let y = {i: 0}; y.i < 3; y.i++) {
a[y.i] = function () {
console.log(y.i);
};
};
a[0]();
a[1]();
a[2]();
你⼀定会好奇,为什么输出的是3个3,不是说let会创建⼀个块级作⽤域吗,我们还是⼀样的改成写模拟代码,如下:
var a = []; {
//我是⽗作⽤域
function怎么记忆let y = {
i: 0
};
if (y.i < 3) {
//这⼀步模拟底层实现
let k = y;
a[k.i] = function () {
//我是⼦作⽤域
console.log(k.i);
};
};
y.i++; //为1
if (y.i < 3) {
let k = y;
a[k.i] = function () {
console.log(k.i);
};
};
y.i++; //为2
if (y.i < 3) {
let k = y;
a[k.i] = function () {
console.log(k.i);
};
};
y.i++; //为3
// 跳出循环
}
a[0](); //3
a[1](); //3
a[2](); //3
注意,在模拟代码中为let k = y⽽⾮let k = y.i。我们始终使⽤let声明⼀个新变量⽤于保存for循环中的初始变量y,以达到创建块级作⽤域的⽬的,即使y是⼀个对象。
那为什么有了块级作⽤域,最终结果还是相同呢,这就涉及了深/浅拷贝的问题。由于y属于引⽤数据类型,let k = y 本质上是保存了变量 y 指向值的引⽤地址,当循环完毕时,y中的 i 已⾃增为3。
变量k因为块级作⽤域的原因虽然也是三个不同的k,但不巧的是⼤家保存的是同⼀个引⽤地址,所以输出都是3了。
我们再次改写代码,说说会输出什么:
var a = []
var b = {i:0};
for (let y = b.i; y < 3; y++) {
a[y] = function () {
console.log(y);
};
};
a[0]();
a[1]();
a[2]();
对深/浅拷贝有疑问可以阅读博主这篇博客
若对JavaScript中基本数据类型,引⽤数据类型的存储有兴趣,可以阅读这篇博客。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。