《你不知道的javascript》循环和闭包的案例

for(var i=1;i<=5;i++){
   setTimeout(function timer(){
       alert(i);
   },i*1000);
}
在《你不知道的javascript》中,是这么描述这段代码的:
缺陷是我们试图假设循环中的每个迭代在运行时都给自己"捕获"一个i的副本。但是根据作用域的工作原理,
实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,
因此实际上只有一个i.

疑问:
只有一个i(为啥i是6)?
能不能这么理解?在函数的5次迭代中,每一次在迭代运行时都给自己捕获了一个i的副本。i排序也就是
1,2,3,4,5,6;但是,又根据作用域的工作原理(词法作用域的查找规则),这6个i中只能是其中一
个i,最后根据i所出现的顺序的先后,6是出现在排序中的最末位,所以这唯一一个i就是6.


阅读 2.8k
4 个回答

第一次进入循环后,设置一个定时器,将Timer函数传入,但此时并不执行,继续往下进入第二次循环。当循环结束之后i=6。然后定时器触发,发现函数中用到i变量,于是浏览器从当前作用域往上找,发现有i变量,值等于6.于是传入的i都等于6

A closure is closed over variable

闭包捕获的是变量,不是值。

认真想想变量和值的区别,就明白了:

循环了5次,产生了5个闭包,这5个闭包都捕获同一个变量i,若干秒后,它们执行的时候,i已经是6了。

你要想捕获一个i的副本,就要想办法从i“复制”一个值到另一个变量,例如用另外一个函数作用域引入这个新的变量:

for(var i=1;i<=5;i++){
   +function(j) {setTimeout(function timer(){
       alert(j);
   },i*1000)}(i);
}

又例如用let,let有块级作用域,每次进入循环块都是一个新的变量:

for(var i=1;i<=5;i++){
   let j = i;
   setTimeout(function timer(){
       alert(j);
   },i*1000);
}

或者简单点:

for(let i=1;i<=5;i++){
   setTimeout(function timer(){
       alert(i);
   },i*1000);
}

所有你理解的“捕获了一个i的副本”是不正确的。

我来帮你捋一下为什么只有一个i,为什么这个i是6

for(var i=1;i<=5;i++){
   setTimeout(function timer(){
       alert(i);
   },i*1000);
}

实际上是:

var i;
for(i=1;i<=5;i++){
   setTimeout(function timer(){
       alert(i);
   },i*1000);
}

var命令发生了”变量提升“现象,导致i泄露成全局变量,每循环一次,全局变量i就会被重新赋值一次,再者此处并未保存每次循环的i值,简单的理解就是每次循环只是设置了一个计时器,5个计时器都是输出全局变量i,当i累加到6时,才停止循环,这个时候全局变量 i的值为6

解决办法有两种

  1. 利用闭包可以保存函数创建时环境的特性,保存每次迭代的i

  2. ES6 的 let 命令

具体实现代码 @oraoto 已给出不赘述

1.的确生成的6个函数复制的作用域链是同一个,每次for循环都改变的是同一个值,在这里也就是共享了全局变量i
2.就是setTimeout会将这6个函数放到异步队列,等待同步的任务执行完成后(此时i已经变成了6),开始依次执行

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题