V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
Newyorkcity
V2EX  ›  问与答

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

  •  2
     
  •   Newyorkcity · 2020-12-27 23:18:16 +08:00 · 2583 次点击
    这是一个创建于 1425 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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 语法糖背后的实际代码不了解也是重要的原因,所以来请教下。感谢。

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

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

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

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

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

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

    觉得这个东西很奇怪只能说是 JS 程序员见怪不怪。一个 var 影响全场才叫怪。
    Elethom
        8
    Elethom  
       2020-12-28 06:51:14 +08:00 via iPhone
    @Arthur2e5
    「强行兼容 let 」这句笑傻了。
    rodrick
        9
    rodrick  
       2020-12-28 08:18:23 +08:00   ❤️ 2
    看一下这个 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
        10
    autoxbc  
       2020-12-28 08:32:50 +08:00 via iPhone
    @Arthur2e5 没错,就是从 C 借来的糟粕,你读读现代 JS,根本没有人用 for,一股 C 味的 JS 最可怕了
    yeship
        11
    yeship  
       2020-12-28 08:36:24 +08:00
    由于 JavaScript 的事件循环,function 回调会在遍历结束后才执行。因为在第一个遍历中遍历 i 是通过 var 关键字声明的,所以这个值是全局作用域下的。在遍历过程中,我们通过一元操作符 ++ 来每次递增 i 的值。当 function 回调执行的时候,i 的值等于 3 。

    在第二个遍历中,遍历 i 是通过 let 关键字声明的:通过 let 和 const 关键字声明的变量是拥有块级作用域(指的是任何在 {} 中的内容)。在每次的遍历过程中,i 都有一个新值,并且每个值都在循环内的作用域中。
    Seanfuck
        12
    Seanfuck  
       2020-12-28 08:42:44 +08:00 via iPhone
    funcs[i]=function(i){……} 试试
    hubqin
        13
    hubqin  
       2020-12-28 08:46:08 +08:00 via Android   ❤️ 1
    了解下 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
        14
    SxqSachin  
       2020-12-28 09:14:47 +08:00   ❤️ 1
    楼上这么多大佬给出的回答居然没有重复的
    我在知乎上看到过一篇文章,感觉解释的比较清楚
    [我用了两个月的时间才理解 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
        15
    yaphets666  
       2020-12-28 09:18:59 +08:00
    @autoxbc
    有时候不得不使用 for 循环
    比如在循环中使用 await 的时候
    autoxbc
        16
    autoxbc  
       2020-12-28 09:40:38 +08:00
    @yaphets666 #15 for ... of 就是现代的 for,可以在其中使用 await 达成目的
    GDC
        17
    GDC  
       2020-12-28 09:42:10 +08:00
    @yaphets666 for...of 和 for...in 都可以 await
    yaphets666
        18
    yaphets666  
       2020-12-28 09:43:58 +08:00
    @autoxbc
    @GDC
    我去试试.我记得以前用的时候没有达到预期的阻塞效果
    SmallTeddy
        19
    SmallTeddy  
       2020-12-28 09:57:02 +08:00
    我建议楼主看一下《你不知道的 JavaScript 》这本书的上卷所讲的作用域部分的知识
    wuzhanggui
        20
    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 循环中那个函数访问的也不同。
    3wdddd
        21
    3wdddd  
       2020-12-28 10:31:05 +08:00
    @yaphets666 这样很慢,应该用 promise.all
    3wdddd
        22
    3wdddd  
       2020-12-28 10:32:24 +08:00
    i 在整个循环内有多个副本,var 只有函数顶部一个
    zankard
        23
    zankard  
       2020-12-28 10:44:07 +08:00 via iPhone
    可以去了解下 js 的 Lexical Environment 。let 是 block scope,每一次 for 迭代都会生成一个新的 Lexical Environment,使得每个迭代使用的 let 变量都不一样;而 var 是 function scope,使用的都是一个变量。
    AmoreLee
        24
    AmoreLee  
       2020-12-28 11:00:03 +08:00
    kx5d62Jn1J9MjoXP
        25
    kx5d62Jn1J9MjoXP  
       2020-12-28 11:10:20 +08:00
    var 不支持 local scope, for 循环定义的 var 是定义在函数体中的
    zckevin
        26
    zckevin  
       2020-12-28 13:03:19 +08:00
    // {
    // let/const x = i;
    // temp_x = x;
    // first = 1;
    // undefined;
    // outer: for (;;) {
    // let/const x = temp_x;
    // {{ if (first == 1) {
    // first = 0;
    // } else {
    // next;
    // }
    // flag = 1;
    // if (!cond) break;
    // }}
    // labels: for (; flag == 1; flag = 0, temp_x = x) {
    // body
    // }
    // {{ if (flag == 1) // Body used break.
    // break;
    // }}
    // }
    // }
    zckevin
        27
    zckevin  
       2020-12-28 15:20:00 +08:00
    《 let/const for loop 的 v8 实现》
    https://zhuanlan.zhihu.com/p/340068236
    Arthur2e5
        28
    Arthur2e5  
       2020-12-29 17:15:03 +08:00   ❤️ 1
    @autoxbc 没有人用 for ? for 的三段形式是对过程式程序循环的最基本表达,有设置循环变量、循环条件、向下一个循环准备的内容。

    最基本的 (i = 0; i < ...; i++) 当然可以用 for of 的 iterator 表达(说 for in 的建议吊起来打),但并不是所有循环都是只有一种 iterable 提供的循环方法的。即使是最基本的 Array 都有倒过来迭代的理由,换到地铁图的图论玩意儿那当然更有。

    是,你可以自己写别的的 iterator 把 [Symbol.iterator] 换了。但是你自己看看那玩意是不是就等于把那三段东西又写了一遍,还得复制粘贴空白模板代码?
    autoxbc
        29
    autoxbc  
       2020-12-29 20:51:24 +08:00
    @Arthur2e5 #28
    for 里的循环变量显示 for 的设计者认为下标访问是对迭代的正确抽象,事实是对迭代的正确抽象是迭代器。迭代器可以设计成正向迭代,自然也可以添加反向迭代方法,这并没有超出设计范围

    ES6 对所有集合体对象部署了迭代器,没有对任何集合体添加新的下标访问接口,原有的基于下标的迭代过程实现为基于迭代器接口,这就是工业界给出的答案
    Arthur2e5
        30
    Arthur2e5  
       2020-12-30 00:12:04 +08:00
    > @autoxbc 下标访问是对迭代的正确抽象

    谁说循环变量是下标了?

    > 正确抽象是迭代器

    你再好好想想,迭代器储存了什么状态?迭代器储存的状态是不是一种循环变量?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2686 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 12:18 · PVG 20:18 · LAX 04:18 · JFK 07:18
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.