时不时更新的前端知识点——let与for循环擦出的火花

前言


关于let使用for循环的现象其实我们都清楚,但是在面试当中假如真让你解释下面的现象,你该如何解释呢?

var arr = [];
for(var i = 0; i < 5; i++){
arr[i] = function(){
console.log(i);
}
}

arr.forEach(fn => fn()); // 5 5 5 5 5


let arr = [];
for(let i = 0; i < 5; i++){
arr[i] = function(){
console.log(i);
}
}
arr.forEach(fn => fn()); // 0 1 2 3 4

最近也看了相关文章,要么是各种长篇大论,要么太过简短,而且个人感觉一些解释还是错误的,所以就查阅了MDN文档,果然找到了答案,只不过注意需要查看英文,中文和英文内容差别真的很大,中文对于for的解释太少了,精髓都在英文文档里。(这里必须要插一句嘴,英语球友们一定也要重视,不说四六级,最起码也得能看懂70%英文文档,计算机常见的英文单词得清楚)

附上英文链接:

for - JavaScript | MDN (mozilla.org)


关于var与for


for(; ;)组成部分:

  1. initalization 初始化
  2. condition 条件
  3. afterthought 操作(英文文档,中文翻译成:final-expression)


for(var i = 0; i < 5; i++){
console.log(i);
}
console.log(window.i); // 5
//--------等价👇--------//

var i = 0;
for(; ;){
if(i >= 5){
break;
}
console.log(i);
i++
}
// var i = 0; 这都是放在了顶级作用域上⭐⭐
console.log(window.i); // 5

可以看到var对于for还是比较单纯的,不管是在for里声明var变量还是在外面,最终都是被挂在到全局GO上,那么就会引出这个经典问题:

var arr = [];
for(var i = 0; i < 5; i++){
arr[i] = function(){
console.log(i);
}
}

arr.forEach(fn => fn()); // 5 5 5 5 5

当执行每个函数时,由于都是同步代码,for循环执行之后由第一个例子可以看出全局下的 i = 5,这时候forEach遍历执行回调时打印出来的都是5


解决方案就很简单了,在ES6之前可以使用闭包,相当于arr里的回调使用了立即执行函数AO里的i形成闭包,5个立即执行函数没有被销毁:

var arr = [];
for(var i = 0; i < 5; i++){
(function(i){
arr[i] = function(){
console.log(i);
}
})(i);

}
arr.forEach(fn => fn()); // 0 1 2 3 4


实际上babel中ES6转ES5也是借助闭包:



关于let与for


如果使用let的话就能轻松解决上面的问题:

let arr = [];
for(let i = 0; i < 5; i++){
arr[i] = function(){
console.log(i);
}
}
arr.forEach(fn => fn()); // 0 1 2 3 4
console.log(window.i); // undefined
console.log(i); // Uncaught ReferenceError: i is not defined❌

不过需要注意的一点let声明的变量并不是放到全局GO上,所以通过全局window对象上是获取不到的。

那么探讨的问题是到底如何实现这样的效果呢?

for的initalization block中用了 let,触发了一种作用域创建机制

每一次迭代的时候会发生以下事情:

  1. 系统会为for loop body -> 创建一个新的词法作用域(lexically scoped
  2. 在词法作用域新声明一个i(let i)
  3. 会将上一次迭代或者初始化的值复制给这个新的i变量
  4. 在新的词法作用域内的i进行操作 i++

其实可以总结出来并不是由于let本身比较特殊,在for循环里每次迭代创建了作用域,而是for循环遇上let,对此产生了这样的特殊机制。

上面的一些理论解释其实已经完全可以作为面试的标准答案来讲,总比问起来只回答一个“let创建的变量是块级作用域”要好一些吧。


模拟上述过程大概就是这个样子:

let arr = [];
for(let i = 0; i < 5; i++){
arr[i] = function(){
console.log(i);
}
}

// ---------模拟循环迭代👇---------- //
let arr = [];
let memory;
{
let i;
i = 0;
arr[i] = function(){
console.log(i);
}
memory++;
}

{
let i;
i = memory; // 1
arr[i] = function(){
console.log(i);
}
}

{
let i;
i = memory; // 2
arr[i] = function(){
console.log(i);
}
memory++;
}

{
let i;
i = memory; // 3
arr[i] = function(){
console.log(i);
}
memory++;
}

{
let i;
i = memory; // 4
arr[i] = function(){
console.log(i);
}
memory++;
}


相关论证


MDN文档其实也给出了几个例子说明了上述问题:

for(let i = 0, getI = () => i; i < 5; i++){
console.log(getI()); // 0 0 0 0 0
}

这里的getI函数拿到的i全是初始化i = 0,而在循环体后续迭代的时候没有拿到新声明的i,因此打印出来的全是0。

但是假如我们把这个getI函数放到afterthought阶段:

for(let i = 0, getI; i < 5; i++, getI = () => i){
console.log(getI);
}
// undefined f() f() f() f()

for(let i = 0, getI = () => i; i < 5; i++, getI = () => i){
console.log(getI());
}
// 0 1 2 3 4

这个结果就比较正常了,getI初始没有赋值,因此第一次为undefined,后续打印就是已赋值的函数。而且这时候拿到的i按照上述步骤已经是重新声明的i了,再次打印的内容就是正常的。




当然这里我通过调试也能够看出点门道:


代码一:



代码二:




注意这里两个代码片段的主要区别就在于声明的let变量位置不同,如果将let放在外面时可以看到是在全局script存在一个i变量,for循环结束后依然存在,最后debugger位置时i为5,那么这个现象就会导致遍历数组打印的值全为5,和之前的var声明变量相同。

而在for循环里声明let变量,它会放在block当中,而且for循环结束后块也消失了。

关于for循环的解释在MDN上还有很多内容,球友们有时间的话可以多花时间看看。




















0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
_Async__
作者分享
前端项目分享:Q Maker——仿问卷星低代码项目
25
2023年度总结:我们的生活没有“如果”
69
一个真实二本学院的秋招:停止内耗,接受自己的平庸
94
求职经验分享——两周的求职历程
22
#蓝桥杯# web 今年的蓝桥杯属实是上压力了,作为一名稀有的web组选手(今年我们软院名单上好像就我一个web组,去年两个[捂脸]),按照去年省赛的难度的话压根要不了4个小时,但刚拿到试卷扫了一眼,瞅到了最后两题的名字就感觉难度上来了。 不过前几道题难度确实不是很大,但很明显给人的感觉就是web组的出题更加成熟了,比如没有像去年那种写两行css就a一道题,纯css的题目像还原设计稿就没有了,不过第一道题的grid布局,确实还挺不错的,如果不是后面有个响应式的题用grid布局的代码我就g了,最后比葫芦画瓢也能写的出来。 其次就是个人感觉削弱了JQuery的比重,jQuery虽然也有使用但没有像去年题目那样带来极大的便利,这次我依旧是原生写JS,感觉使不使用差别不是特别大。 只能说有点水平的就是最后两题:markdown解析、树形结构拖拽 markdown的还好,一是看正则基础,二是对于blockquote和ul需要费点功夫,看得出来web组对于正则的要求一直都是有的。 最后一道就有点东西了,个人在上面那道题费了太多时间,最后一道时间明显不太够了只写了送分的那一问。大概看了看应该是原生手写递归组件,之前只用vue写过类似多级评论,递归菜单这样的。虽然原理都一样但是原生还是有点虚,第三问应该就是类似树形结构查找,扁平化删改这样,当时也没仔细看,一会儿扒扒原题再研究研究。 整体来讲还是比较考验基础的,像使用框架的题目分值就那么多,压轴的题永远是看你的原生JS能力。难度确实比去年省赛的题目大,但个人感觉这才是正常的比赛,去年省赛和国赛的难度差别过大反而不太适应,今年听天由命得了,也是最后一次了,可能要捐款了。。
19
下载 APP