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

353 天前
 takeshima

本人 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))
  }
}

求大佬指点

2132 次点击
所在节点    JavaScript
19 条回复
john2022
353 天前
这个和闭包有关么?
realJamespond
353 天前
fn2 不死循环?
GentleFifth
353 天前
可以先理解作用域,理解了作用域就理解了闭包
molvqingtai
353 天前
第二个有结果吗?
Drumming
353 天前
GPT4 的回答: https://short.aiayw.com/lqltq7
GPT3.5 的回答: https://short.aiayw.com/iacpzf
仅供参考
takeshima
353 天前
@realJamespond 不好意思,i++打掉了😳
takeshima
353 天前
setTimeout 里面的那个函数捕获了外层的 i 变量,应该是闭包吧,我就比较好奇为什么这两个函数一个 i 跟着外层变了,一个没变
rabbbit
353 天前
let i = 0
for (; i < 10; i++) {
setTimeout(() => console.log(i))
}

https://es6.ruanyifeng.com/#docs/let
molvqingtai
353 天前
我猜你是想问这个?

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

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

下面这块估计你是漏写了一个 i++,这里涉及到 settimeout 是宏任务异步执行的问题,只有 while 循环结束之后,settimeout 里的 console.log 才会去取 i 值,结果就是取到了所有 i++ 执行完之后的值了
rabbbit
353 天前
takeshima
353 天前
@ochatokori 感谢解答,居然还真是这样,每次循环的 i 居然是一个新的 i ,太反直觉了😂
realJamespond
353 天前
settimeout 相当于是多线程,应该把主线程的值 bind 到子线程的函数作为参数,根据 c+的理解
CLMan
353 天前
第二种是任何语言中都可能会出现,都要避免的情况。闭包捕获了外部变量`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
353 天前
修正:比如 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
353 天前
问题主要出在块级作用域上
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
353 天前
@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
352 天前
闭包的问题就是作用域的问题。把 js 的 GC 和作用域搞明白了,再去看闭包就很好理解了

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

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

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

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

© 2021 V2EX