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 · 2429 次点击
    这是一个创建于 597 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本人 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   ·   实用小工具   ·   1153 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 18:28 · PVG 02:28 · LAX 10:28 · JFK 13:28
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.