基本概念:编译器(Compiler)、解释器(Interpreter)、抽象语法树(AST)、字节码(Bytecode)、即时编译器(JIT)
1.编译器和解释器
因为机器不能直接读懂我们所写的代码,所以我们需要一个“中间人”来为机器进行“翻译”,根据翻译机制的不同,语言划分为编译型语言和解释型语言,哪么所对应的“中间人”就可以分为编译器和解释器。编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如 C/C++、GO 等都是编译型语言。而由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。比如 Python、JavaScript 等都属于解释型语言。

在编译型语言的编译过程中,编译器首先会依次对源代码进行词法分析、语法分析,生成抽象语法树(AST),然后是优化代码,最后再生成处理器能够理解的机器码。如果编译成功,将会生成一个可执行的文件。但如果编译过程发生了语法或者其他的错误,那么编译器就会抛出异常,最后的二进制文件也不会生成成功。
在解释型语言的解释过程中,同样解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。
2.V8 是如何执行一段 JavaScript 代码的

1. 生成抽象语法树(AST)和执行上下文
抽象语法树(AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。抽象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采用的上下文无文文法,因为在写文法时,经常会对文法进行等价的转换(消除左递归,回溯,二义性等),这样会给文法分析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得混乱。因些,很多编译器经常要独立地构造语法分析树,为前端,后端建立一个清晰的接口。
可以通过下面的代码生成的AST直观的看出它的结构
var a = 42;
var b = 5;
function addA(d) {
return a + d;
}
var c = addA(2) + b;
抽象语法树的生成分为两个阶段
- 词法分析(tokenize):其作用是将一行行的源码拆解成一个个 token。所谓 token,指的是语法上不可能再分的、最小的单个字符或字符串。
- 语法分析(parse):其作用是将上一步生成的 token 数据,根据语法规则转为 AST。如果源码符合语法规则,这一步就会顺利完成。但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。
有了抽象语法树,接下来 V8 就会生成该段代码的执行上下文。执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等,相信大家已经很熟悉,就不过多介绍了。
2. 生成字节码
其实一开始 V8 并没有字节码,而是直接将 AST 转换为机器码,由于执行机器码的效率是非常高效的,所以这种方式在发布后的一段时间内运行效果是非常好的。但是随着 Chrome 在手机上的广泛普及,特别是运行在 512M 内存的手机上,内存占用问题也暴露出来了,因为 V8 需要消耗大量的内存来存放转换后的机器码。为了解决内存占用问题,V8 团队大幅重构了引擎架构,引入字节码,并且抛弃了之前的编译器,最终花了将进四年的时间,实现了现在的这套架构。
字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。
3. 执行代码
通常,如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。在 执行字节码的过程中,如果发现有热点代码(HotSpot,比如一段代码被重复执行多次,这种就称为热点代码)那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。
字节码配合解释器和编译器这种技术称为即时编译(JIT)。具体到 V8,就是指解释器 Ignition 在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,TurboFan 编译器便闪亮登场,把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。具体过程对应上图(V8 执行一段 JavaScript 代码的流程)中橙色部分。
总结
综上,可以将V8执行JavaScript代码的过程概括为如下:因为JavaScript为解解释性语言,一般通过解释器进行执行。首先通过词法分析,将一行行代码转化为token,通过语法分析构建抽象语法树(AST),然后根据AST生成执行上下文,通过解释器将AST转化为字节码,利用解释器边解释边执行字节码。V8对JavaScript代码的执行做出了一些性能上的优化,会通过代码被执行的次数判断是否为热点代码,将热点代码保存为机器代码,直接利用编译器编译执行执行。我们把这种执行方式称为即时编译(JIT)