js 中 for 循环的语法糖解开是什么样的?为什么 let 和 var 导致输出结果不同?

2020-12-27 23:18:16 +08:00
 Newyorkcity
function foo() {
    var funcs = [];
    for (var i = 0; i < 3; i++) { //此处使用 var i = 0
        funcs[i] = function () {
            console.log("funcs - " + i);
        };
    }
    for (var j = 0; j < 3; j++) {
        funcs[j]();
    }
}
foo();

输出结果:
funcs - 3
funcs - 3
funcs - 3

function foo() {
    var funcs = [];
    for (let i = 0; i < 3; i++) { //此处使用 let i = 0
        funcs[i] = function () {
            console.log("funcs - " + i);
        };
    }
    for (var j = 0; j < 3; j++) {
        funcs[j]();
    }
}
foo();

输出结果:
funcs - 0
funcs - 1
funcs - 2

我知道 let 的作用域被 {} 限制,而 var 的作用域等于其所在的函数的函数体,但就从这个知识点我不能解释上述情况出现的原因。我觉得其中我对 for 语法糖背后的实际代码不了解也是重要的原因,所以来请教下。感谢。

2378 次点击
所在节点    问与答
30 条回复
chashao
2020-12-27 23:28:21 +08:00
应该是去了解一下闭包的原理吧,闭包保存的是上下文,所以上下文里面的变量 var 只被创建一次,但是 let 每次循环都会重新创建新的变量(前面都是我乱写
hyuka
2020-12-27 23:32:39 +08:00
不就是因为你说的吗,上面的实现在输出时 i 就为 3 了;再 funcs 调用就输出了 3,接下来的循环 i 没变,一直为 3
anguiao
2020-12-28 00:04:42 +08:00
上面的 funcs[0/1/2]指向的都是同一个“i”,循环结束后就变成 3 了。后面打印的时候,因为 i 已经是 3 了,所以输出全都是 3 。
Newyorkcity
2020-12-28 00:36:46 +08:00
@hyuka
@anguiao

所以怎么解释 let 的情况不一样?
autoxbc
2020-12-28 00:44:40 +08:00
for 设计的时候还没有 let,等 let 出现后 for 有了新的语义,即每个循环节互相隔离,let 初始化的变量在每个循环节有个副本,取值只对当前循环节有效

这样的设定是强行兼容 let,尤其是 let 并不在块语句中这个现实,还有 let 自增后值可以跨循环节保持也反直觉

我觉得 for 是旧时代的糟粕,函数式和迭代器才是未来,应该多用 forEach 和 for ...of
anguiao
2020-12-28 00:45:29 +08:00
@Newyorkcity 因为每次循环其实都创建了一个新的“i”,所以绑定的就是每次循环时 i 的值。
Arthur2e5
2020-12-28 06:33:40 +08:00
> @autoxbc 这样的设定是强行兼容 let,尤其是 let 并不在块语句中这个现实

这完全(语气助词)正常,早就有语言这么做。这个 for 的行为和以下的 C99 代码行为是一致的:

```C
for (int i = 0; i < 3; i++) /* blah */;
```

觉得这个东西很奇怪只能说是 JS 程序员见怪不怪。一个 var 影响全场才叫怪。
Elethom
2020-12-28 06:51:14 +08:00
@Arthur2e5
「强行兼容 let 」这句笑傻了。
rodrick
2020-12-28 08:18:23 +08:00
看一下这个 https://es6.ruanyifeng.com/#docs/let
其实明白两点就好了:
1. var 的循环公用一个 i
2. let 的循环每次都是新的 i
3. JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量 i 时,就在上一轮循环的基础上进行计算。

至于作用域问题,for 那块是一个作用域,下面的{}又是一个子作用域,互不干扰:
```
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
```
这个打印的都是 "abc" 也不会报 i 已经声明的错
autoxbc
2020-12-28 08:32:50 +08:00
@Arthur2e5 没错,就是从 C 借来的糟粕,你读读现代 JS,根本没有人用 for,一股 C 味的 JS 最可怕了
yeship
2020-12-28 08:36:24 +08:00
由于 JavaScript 的事件循环,function 回调会在遍历结束后才执行。因为在第一个遍历中遍历 i 是通过 var 关键字声明的,所以这个值是全局作用域下的。在遍历过程中,我们通过一元操作符 ++ 来每次递增 i 的值。当 function 回调执行的时候,i 的值等于 3 。

在第二个遍历中,遍历 i 是通过 let 关键字声明的:通过 let 和 const 关键字声明的变量是拥有块级作用域(指的是任何在 {} 中的内容)。在每次的遍历过程中,i 都有一个新值,并且每个值都在循环内的作用域中。
Seanfuck
2020-12-28 08:42:44 +08:00
funcs[i]=function(i){……} 试试
hubqin
2020-12-28 08:46:08 +08:00
了解下 js 的变量提升,所有 var 声明都会被提取到文件或函数开头,上面用 var 的循环,相当于在循环前面连续声明了 3 次 i,var i=1;var i=2;var i=3; i 是个全局变量,最终内存地址中 i 指向的值是 3 。而 let 声明的变量是块级作用域的,每个 {} 都是一个块级作用域,每次 for 也是一个块级作用域,所以 for... let...循环,声明了三次 i,值分别为 1 2 3
SxqSachin
2020-12-28 09:14:47 +08:00
楼上这么多大佬给出的回答居然没有重复的
我在知乎上看到过一篇文章,感觉解释的比较清楚
[我用了两个月的时间才理解 let]( https://zhuanlan.zhihu.com/p/28140450)
[Runtime Semantics: LabelledEvaluation]( http://www.ecma-international.org/ecma-262/6.0/#sec-for-statement-runtime-semantics-labelledevaluation)
yaphets666
2020-12-28 09:18:59 +08:00
@autoxbc
有时候不得不使用 for 循环
比如在循环中使用 await 的时候
autoxbc
2020-12-28 09:40:38 +08:00
@yaphets666 #15 for ... of 就是现代的 for,可以在其中使用 await 达成目的
GDC
2020-12-28 09:42:10 +08:00
@yaphets666 for...of 和 for...in 都可以 await
yaphets666
2020-12-28 09:43:58 +08:00
@autoxbc
@GDC
我去试试.我记得以前用的时候没有达到预期的阻塞效果
SmallTeddy
2020-12-28 09:57:02 +08:00
我建议楼主看一下《你不知道的 JavaScript 》这本书的上卷所讲的作用域部分的知识
wuzhanggui
2020-12-28 10:13:39 +08:00
for 循环只是一个代码块(内部分为了多个),var 变量不区分代码块,let 要区分,同一个代码块中生命多次 var a 相当于给第一次 var 的 a 重新赋值而已,所以 for (var i = 0; i < 3; i++)相当于 var i = 0;for (i = 0; i < 3; i++),而 let 就不同了,相同代码块中声明名字相同的 let 变量是不允许的,所以每个代码块中的同名称变量不同。所以 for 循环中那个函数访问的也不同。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/739474

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX