let 与 var 在 for 循环中的区别
本文最后更新于:2020年5月9日 早上
场景
今天遇到的一个很有趣的问题,下面两段 js 代码执行的结果是什么?
1 |
|
和
1 |
|
嗯,乍看之下好像没什么区别,只有声明方式 let
和 var
不一样而已。
分析
这里先说一下吾辈两个关于 js 的认知
- js 里
setTimeout
如果延迟时间为 0 应该会立刻执行 - js 里的 for 循环和 java 应该差不多,for 循环内部是单独的作用域
图解如下
那么答案只有一个,两段代码执行的结果应该都是 0 1 2
才对!O(≧▽≦)O
然而当吾辈执行后的结果却是
let
:0 1 2
var
:3 3 3
发生了什么?吾辈表示很无语。。。┐( ̄ヮ ̄)┌
解答
然而,上面的两个认知全错了!
其一:js 里 setTimeout
如果延迟时间为 0 应该会立刻执行
好吧,异步没有 立刻执行 这个说法,js 中异步函数实际上是被 事件队列 所管理的。当使用 setTimeout
函数时,即便延迟为 0,函数 () => console.log(i)
也不会立刻执行,而是会被放到 事件队列 中去,然后等待浏览器空闲之后执行。
在 MDN 上有一段关于零延迟的描述
零延迟
零延迟并不意味着回调会立即执行。以 0 为第二参数调用
setTimeout
并不表示在0
毫秒后就立即调用回调函数。
其等待的时间取决于队列里待处理的消息数量。在下面的例子中,”this is just a message” 将会在回调获得处理之前输出到控制台,这是因为延迟参数是运行时处理请求所需的最小等待时间,但并不保证是准确的等待时间。
基本上,setTimeout
需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间。
所以 setTimeout
实际上并没有立刻执行,而是等到整个 for
循环结束之后才执行的。
其二:js 里的 for 循环和 java 应该差不多,for 循环内部是单独的作用域
好吧,这个认知更是错的一塌糊涂,for 循环居然没有块级作用域?i 和 k 都是可以直接访问的,犹如直接声明到 for 循环外一样。
1 |
|
相当于
1 |
|
如果换成 let 则两者都无法访问
1 |
|
甚至还有一个更有趣的情况,在 for 的表达式和块中可以声明相同的变量,这只说明了一件事,let 声明的变量和循环内部声明的变量不在同一个作用域中!
1 |
|
或许,i 只是加了新的作用域,就像下面这样,如此,循环外面就访问不到内部的值,循环内部和 for 的表达式也同样不在一个作用域了,每次循环结束就更新这个值
1 |
|
附:这里吾辈是根据 babel 编译的结果修改而来。而且 babel 真的很聪明,当迭代变量 i 没有更新时,就不会使用
_i
进行区分呢!
解决
重新建立了自己的认知之后,可以再对 let/var
在 for 循环进行分析了。
首先是 let + for
let + for
再看下面这段代码,可以对其进行分解
1 |
|
- 创建 for 循环,表达式中存在 let 变量,for 将会创建一个块级作用域(ES6 let 专用)
- 每次迭代时,会创建一个子块级作用域,迭代变量 i 也会重新生成
- 对 i 的任何操作,都会被记住并赋值给下一次的迭代
块级作用域只对 let 有效,var 声明的变量仍然能在 for 循环外使用,证明 for 循环并不是像函数作用域那样是连 var 都能封闭的作用域。
图解如下
var + for
分析一下
1 |
|
- 进入 for 循环
- 在这里创建了迭代变量 i,因为是函数作用域变量所以在 for 循环外可以访问,被提升到了函数作用域顶部声明
- setTimeout 函数执行,闭包绑定函数作用域外部变量 i,在循环结束输出 i 的值 3
- 继续迭代
所以以后如果可能,还是要拥抱这些新特性呢!那么,关于 let/var
在 for
循环中的区别就到这里啦