求解一个关于闭包的 JS 代码的问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a Javascript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
Javascript 权威指南第 5 版
Closure: The Definitive Guide
takeshima
V2EX    Javascript

求解一个关于闭包的 JS 代码的问题

  •  
  •   takeshima 2023-05-10 21:51:57 +08:00 via iPhone 2944 次点击
    这是一个创建于 883 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本人 JS 新手,最近学到闭包,还没太弄明白,不懂以下两个函数的运行结果为何不同

    const fn1 = () => { for (let i = 0; i < 10; i++) { setTimeout(() => console.log(i)) } } const fn2 = () => { let i = 0 while (i < 10) { setTimeout(() => console.log(i)) } } 

    求大佬指点

    19 条回复    2023-05-11 13:44:56 +08:00
    john2022
        1
    john2022  
       2023-05-10 21:54:31 +08:00
    这个和闭包有关么?
    realJamespond
        2
    realJamespond  
       2023-05-10 21:58:28 +08:00
    fn2 不死循环?
    GentleFifth
        3
    GentleFifth  
       2023-05-10 21:59:29 +08:00 via Android
    可以先理解作用域,理解了作用域就理解了闭包
    molvqingtai
        4
    molvqingtai  
       2023-05-10 21:59:35 +08:00
    第二个有结果吗?
    Drumming
        5
    Drumming  
       2023-05-10 22:00:18 +08:00
    GPT4 的回答: https://short.aiayw.com/lqltq7
    GPT3.5 的回答: https://short.aiayw.com/iacpzf
    仅供参考
    takeshima
        6
    takeshima  
    OP
       2023-05-10 22:00:21 +08:00
    @realJamespond 不好意思,i++打掉了
    takeshima
        7
    takeshima  
    OP
       2023-05-10 22:01:41 +08:00
    setTimeout 里面的那个函数捕获了外层的 i 变量,应该是闭包吧,我就比较好奇为什么这两个函数一个 i 跟着外层变了,一个没变
    rabbbit
        8
    rabbbit  
       2023-05-10 22:01:50 +08:00   2
    let i = 0
    for (; i < 10; i++) {
    setTimeout(() => console.log(i))
    }

    https://es6.ruanyifeng.com/#docs/let
    molvqingtai
        9
    molvqingtai  
       2023-05-10 22:02:09 +08:00
    我猜你是想问这个?

    for (var i = 0; i < 10; i++) {
    setTimeout(() => console.log(i))
    }

    for (let i = 0; i < 10; i++) {
    setTimeout(() => console.log(i))
    }
    takeshima
        10
    takeshima  
    OP
       2023-05-10 22:02:50 +08:00
    第二个函数 i++掉了,应该是这个
    const fn2 = () => {
    let i = 0
    while (i < 10) {
    setTimeout(() => console.log(i))
    i++
    }
    }
    ochatokori
        11
    ochatokori  
       2023-05-10 22:02:54 +08:00 via Android   1
    for 那段是相当于把 i 这个变量扔到大括号里面声明再初始化,而又因为 let 是块级作用域的特性,相当于 for 多少次就声明多少个,自然值就不一样。

    下面这块估计你是漏写了一个 i++,这里涉及到 settimeout 是宏任务异步执行的问题,只有 while 循环结束之后,settimeout 里的 console.log 才会去取 i 值,结果就是取到了所有 i++ 执行完之后的值了
    rabbbit
        12
    rabbbit  
       2023-05-10 22:12:20 +08:00
    takeshima
        13
    takeshima  
    OP
       2023-05-10 22:13:42 +08:00 via iPhone
    @ochatokori 感谢解答,居然还真是这样,每次循环的 i 居然是一个新的 i ,太反直觉了
    realJamespond
        14
    realJamespond  
       2023-05-10 22:58:07 +08:00
    settimeout 相当于是多线程,应该把主线程的值 bind 到子线程的函数作为参数,根据 c+的理解
    CLMan
        15
    CLMan  
       2023-05-11 01:29:00 +08:00
    第二种是任何语言中都可能会出现,都要避免的情况。闭包捕获了外部变量`i`,输出的结果取决执行时,`i`的即时值。避免的办法是创建一个块作用域的复制值,但是存在心智负担。

    Javascript 采用单线程模型,因为循环中没有中断当前执行流的逻辑,因此所有 timeout 处理逻辑只有等循环结束后才能执行,此时 i 的值为 10 ,所以输出`10`共 10 次。

    严谨的语言会对这种情况进行语法限制,避免不经意间写出 bug 。比如 Java ,会要求被捕获的值必须是 final 或者等价 final 的:
    ```java
    for (int i = 0; i < 10; i++) {
    int num = i;// 这里使用一个等价 final 的块作用域变量
    new Thread(() -> System.out.println(num)).start(); // 0 9 8 7 5 3 4 2 6 1
    }
    ```

    需要刻意的使用容器,比如数组,来实现第二种的效果,Java 因为是多线程语言,子线程与主线程是并发执行,输出结果不全是`10`:
    ```java
    final int[] ref = {0};
    for (int i = 0; i < 10; i++) {
    new Thread(() -> System.out.println(ref[0])).start();// 3 4 4 3 8 6 10 10 10 10
    ref[0]++;
    }
    ```

    Javascript 是一门存在许多设计缺陷的语言,es6 进行了许多修补。第一种就是 es6 对第二种情况容易产生 BUG 的修补,它的思路与 Java 是不同的:`而在使用 let 声明迭代变量时,Javascript 引擎在后台会为每个迭代循环声明一个新的迭代变量`,因此 fn1 里面的`setTimeout()`每次捕获的变量的值都是循环时的值。

    Javascript 采用单线程执行模型,`Javascript 维护了一个任务队列。其中的任务会按照添加到队列的先后顺序执行`。`setTimeout()`省略`delay`表示立即提交到任务队列,因为顺序提交,因此顺序输出。

    可以看看《 Javascript 程序高级程序设计》《 understanding es6 》。我忘得差不多了,也是看到这个问题才去看书回忆起这些细节。

    写了一个第二种存在中断循环的版本,看不懂也没啥关系:
    ```
    const fn2 = async () => {
    let i = 0;
    for (; i < 10; ) {
    setTimeout(() => console.log(i))
    i = await plusOne(i)
    }
    }

    function plusOne(n){
    return new Promise(function(resolve, reject) {
    setTimeout(() => {
    resolve(n+1)
    },1)
    })
    }

    fn2() // 0 1 2 3 4 5 6 7 8 9
    ```
    CLMan
        16
    CLMan  
       2023-05-11 01:45:04 +08:00
    修正:比如 Java ,会要求被捕获的**变量**必须是 final 或者等价 final 的

    补充:es5 没有块作用域,只有全局作用域和函数作用域,也就是:
    ```
    const fn1 = () => {
    let i = 0;
    for (; i < 10; i++) {
    var j = i;// 使用 var 定义的 J 是函数作用域
    setTimeout(() => console.log(j))
    }
    }

    fn1()//9 9 9 9 ...
    ```
    当然现在都是用`let`了,这些过时的知识没有太多了解的必要,我也忘得差不多了。
    tsja
        17
    tsja  
       2023-05-11 09:20:34 +08:00
    问题主要出在块级作用域上
    let 会形成块级作用域, 如果把 fn1 修改成 var 定义的变量, 就和 fn2 效果一样了
    const fn1 = () => {
    for (var i = 0; i < 10; i++) {
    setTimeout(() => console.log(i))
    }
    }
    // 打印十个 10

    因为 for 循环中, 每个 let 都是自己的块级作用域, 把这个 for 循环展开的结果是这样:
    const fn1 = () => {
    {let i = 0
    setTimeout(() => console.log(i))
    }
    {let i = 0
    setTimeout(() => console.log(i))
    }
    }
    tsja
        18
    tsja  
       2023-05-11 09:22:12 +08:00
    @tsja 误操作, 没写完直接发送了, 接着说:

    因为 for 循环中, 每个 let 都是自己的块级作用域, 把这个 for 循环展开的结果是这样:
    const fn1 = () => {

    {let i = 0
    setTimeout(() => console.log(i))
    }

    {let i = 1
    setTimeout(() => console.log(i))
    }

    {let i = 2
    setTimeout(() => console.log(i))
    }

    ....
    }

    推荐阅读《你不知道的 Javascript 》上卷, 关于作用域和闭包问题讲的挺好的
    cangcang
        19
    cangcang  
       2023-05-11 13:44:56 +08:00
    闭包的问题就是作用域的问题。把 js 的 GC 和作用域搞明白了,再去看闭包就很好理解了
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3715 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 47ms UTC 00:51 PVG 08:51 LAX 17:51 JFK 20:51
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86