for(var i=0;i<10;i++){
setTimeout(()=>{
console.log(i)
},100)
}
如上图,这个函数会打印 10次 10 的原因我是知道的,在 for的小括号(var i=0 )
相当于声明了一个全局变量。
但是疑问的点在下: 当我声明时换成了 let
for(let i=0;i<10;i++){
setTimeout(()=>{
console.log(i)
},100)
}
那么结果就是 0,1,2...
这结果的原因大概我知道是 let 块级作用域造成的,但是我无法总结比较好的语言去解释这一现象。
问题一:请问上图 let 造成的现象该如何准确描述呢?
问题二:如果说 for
的小括号声明的变量是同一作用域,那 let 不是应该不允许重复声明吗?
问题三:站在编译器的角度,for 循环的原理是什么呢?
还望各位先生不吝赐教
在回答问题之前必须得说明这些现象不是编译器的feature而是编译器按照es标准去实现了这些现象至于具体是怎么实现的这完全取决于编译器作者,不同的js engine之间的实现思路可能大相径庭,所以在这里如果你不想了解底层实现,则只需要记住平时看到的解释比如
而使用 let 声明时,是每一个循环独立的变量 i。
在这里我以quickjs(一个实现了ES2020的嵌入式js engine)为例解释一下上面的现象是如何实现在js engine中实现的。1. 字节码
quickjs会把javascript先编译为字节码然后才开始运行, 字节码就像是更高层次的汇编,普通的汇编(在这里忽略的汇编为机器码这一步)是直接运行在x86,arm等平台的机器上的,而这里的js字节码是运行在quickjs虚拟机上的,至于为什么需要字节码,以及什么是字节码请自行了解,基本上要了解let,var有什么区别只去要看一下他们生成的字节码有什么区别即可
2. var生成的字节码
对于
生成的字节码为
3. let生成的字节码
4. 字节码对比
字节码很长我们主要关注put_loc指令这个指令就代表给局部变量赋值,如果我们仔细对比一下就会发现每次对i赋值后(在代码中i = 0;和i++),let版本的字节码中会多一条
close_loc
指令让我们到源码里看看close_loc干了些什么,经过寻找我们可以看到虚拟机在碰到
close_loc
指令是会去执行close_lexical_var函数其中
var_ref->value = JS_DupValue(ctx, sf->var_buf[var_idx]);
这一句代码复制了一遍变量i的值,注意在这里i是个数字所以var_buf[var_index]
中存的就是数字本身,如果是引用类型存的是一个指针,所以真相大白,每次对i的修改都会复制一份当前的变量值,所以每次在遇到fclosure8
指令创建闭包时,就能捕获到一个新复制变量,这个变量保存着当前i迭代次数的值下面我们可以正式开始回答你问的三个问题了
问题一:请问上图 let 造成的现象该如何准确描述呢?
正如网上所描述的你可以理解为每依次for循环都i是一个全新的变量,当然这种情况只对能放在数字这种原始值有效比如下面的代码,还是会打印十次10,应为它十次循环复制的十个指针,这十个指针完全指向相同的值,而是当数字时,由于可以直接放在变量插槽中所以复制的数字本身
问题二:如果说 for 的小括号声明的变量是同一作用域,那 let 不是应该不允许重复声明吗?
这种限制往往只是js语言层面上的,对于编译器实现者来说不会存在这种限制,如果你了解计算机体系模型就会了解到一种存在于上层应用的限制在下一层往往不存在,最好的例子你可以了解一下java中
volatile与内存屏障
上层看起来习以为常的东西,它的底层未必是问题三:站在编译器的角度,for 循环的原理是什么呢?
在编译器中for会被编译成目标平台的逻辑跳转指令。在这里,我们以x86为例
一个打印1-9的C代码是这样的,在这里我故意不用for语句而把他写成等价的goto语句,方便对比
for(init; test; update) body
他对应的汇编是这样的(删除了无关的语句),可以看到基本逻辑和上面的C代码无异