本人 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))
}
}
求大佬指点
1
john2022 2023-05-10 21:54:31 +08:00
这个和闭包有关么?
|
2
realJamespond 2023-05-10 21:58:28 +08:00
fn2 不死循环?
|
3
GentleFifth 2023-05-10 21:59:29 +08:00 via Android
可以先理解作用域,理解了作用域就理解了闭包
|
4
molvqingtai 2023-05-10 21:59:35 +08:00
第二个有结果吗?
|
5
Drumming 2023-05-10 22:00:18 +08:00
|
6
takeshima OP @realJamespond 不好意思,i++打掉了😳
|
7
takeshima OP setTimeout 里面的那个函数捕获了外层的 i 变量,应该是闭包吧,我就比较好奇为什么这两个函数一个 i 跟着外层变了,一个没变
|
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 |
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)) } |
10
takeshima OP 第二个函数 i++掉了,应该是这个
const fn2 = () => { let i = 0 while (i < 10) { setTimeout(() => console.log(i)) i++ } } |
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++ 执行完之后的值了 |
12
rabbbit 2023-05-10 22:12:20 +08:00
|
13
takeshima OP @ochatokori 感谢解答,居然还真是这样,每次循环的 i 居然是一个新的 i ,太反直觉了😂
|
14
realJamespond 2023-05-10 22:58:07 +08:00
settimeout 相当于是多线程,应该把主线程的值 bind 到子线程的函数作为参数,根据 c+的理解
|
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 ``` |
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`了,这些过时的知识没有太多了解的必要,我也忘得差不多了。 |
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)) } } |
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 》上卷, 关于作用域和闭包问题讲的挺好的 |
19
cangcang 2023-05-11 13:44:56 +08:00
闭包的问题就是作用域的问题。把 js 的 GC 和作用域搞明白了,再去看闭包就很好理解了
|