课本上的编译原理
任何编程语言是无法直接运行在机器上的,想要将我们编写的代码翻译为机器可以识别运行的代码,需要对编程语言进行“翻译”。回顾一下学校的编译原理课本,按语言的执行流程划分,编程语言可以被划分为两种:
- 编译型语言
- 解释型语言
对比二者“翻译”的流程,其主要区别在于,编译型语言生成的是可以直接执行的二进制文件,而解释型语言则需要借助解释器,对生成的字节码逐行解释执行。
JavaScript是如何被执行的?
JavaScript的“翻译”过程如上图所示,相比较课本上传统的划分方法,最主要的特点在于,JavaScript在执行的过程中,同时用到了编译器与解释器。为什么JavaScript的执行过程要这样设计?
抽象语法树(AST)
浏览器不能理解HTML,需要将HTML转换为浏览器能够理解的结构DOM树。同理,JavaScript也无法直接被编译器或解释器理解,需要将其转为抽象语法树,才能够被编译器或解释器理解并处理。抽象语法树的生成主要分为两步:
- 分词(词法分析):将源码解析为语法上不可再分的字符串(称为token);
- 解析(语法分析):将token按语法规则转换为抽象语法树; 通过以上两步生成的抽象语法树,本质是一种代码的结构化表示,借助这种表示形式,可以进行一系列基于语法规则的操作。例如,Babel就利用了抽象语法树作为中转,将ES6的代码转为抽象语法树后,再利用ES5的语法规则,将抽象语法树还原为ES5的代码。
字节码
为什么JavaScript已经通过解释器生成了字节码,还要多此一举,引入编译器字节码编译为机器码?或者干脆直接砍掉解释器,完全使用编译器执行不可以吗?
要回答这个问题,首先需要明确,JavaScript大多执行在浏览器上,而浏览器需要运行在诸多不同种类,不同大小的设备上。例如,在移动端设备上,内存资源十分宝贵,而如果直接将抽象语法树编译为机器码,需要消耗大量的内存资源,引发内存占用。因此,引入字节码成为了一个可选项。字节码相较机器码更高级,同一段语义,高级语言相较低级语言,可以用更少的字符表示,即单位语义内所需要的内存更少。
但解释需要一行一行执行,其执行效率远低于直接执行编译生成的机器码。为了兼顾效率,JavaScript在执行时引入了热点代码。当一段代码被解释器重复执行多次,则会变为热点代码,其含义是在程序中可能被频繁执行的代码片段。为了提高执行效率,热点代码会编译器一次性的直接编译为机器码,并保存下来供重复使用。
将字节码配合编译器与解释器进行执行的技术,称为即时编译技术(JIT)。