(一)前言
前端程序员对JavaScript这门编译语言应该是很熟悉了。一门传统的编译语言在其代码执行之前会经历分词/词法分析、解析/语法分析、代码生成三步,这三步统称为”编译“。JavaScript 与传统的语言不同之处是编译工作没有发生在构建之前,其代码在要被执行前才会进行编译,所以 JavaScript 被归类为”动态“或”解释执行“语言。这里简要的介绍一下JavaScript的编译原理。
(二)编译原理
通常来说我们写的代码就是一长串的字符串(后面统称为长串的字符串),程序员在借助开发工具才有了格式一说。机器能能够识别的语言只有0和1,后面简称机器语言。JavaScript 引擎在代码与机器语言间承担“翻译”的工作,为了便于理解,后面拿 V8 引擎进行举例说明,简要将编译过程绘制一个流程图表,如下图:
(1)分词/词法分析
分词是将我们编写的一长串的字符串分解成词法单元。例如 var a = 10
分词后结果为 ["var","a","=","10"]
这个数组里的元素就是词法单元(这里是为了便于理解,实际并不一定是用数组的数据结构类型)。如果词法单元中的元素是其它词法单元中的一部分,就会进行词法分析。例如:
var a = 10
function fn(a){
console.log(a)
}
fn(1)
上述代码中我们能发现 a
被多次用到。如果用人物动作描述方式将承担编译工作的执行者称为“编译者”,那么编译者在进行分词的过程中也发现a
是其它词元单元中的一部分,这个时候就会将a
进行词法分析。
(2)解析/语法分析
编写的一长串的字符串在经过了分词/词法分析的步骤后产生出词法单元流,对其进行解析/语法分析后产生的结果就是抽象的语法树,它有一个我们比较熟悉的名字就是AST(Abstract Syntax Tree)。参考下图:
上图语法树中的 start 和 end 是全局一长串的字符串的索引起始和结束位置,body中每个元素根据声明类型不同其包含的元素也会不同,如果对 AST 感兴趣可以访问 astexplorer.net 体验及研究。
(3)代码生成
前面也提到过,代码生成的步骤就是将 AST 转换成机器能识别的语言。在这个步骤中有两个角色需要特别说明一下,分别是 Interpreter 和 Compiler。Interpreter 的优点是无需等待编译即可立即执行代码,这个优点也为 JavaScript 这种现编译现执行的需求提供了技术支撑;其缺点是逐行将源代码转换为等效的机器代码,在有大量的JavaScript 的代码时运行性能会慢下来,因为中间还需要 V8 引擎再次转译。Compiler 的优点是在一开始就将所有源代码转换为机器代码,并且对代码进行了优化处理;其缺点之一就是需要一定的时间等待。Interpreter 生成 ByteCode 并且还需要 V8 引擎进行中间代理转译才能真正的生成机器语言。Compiler 生成优化后的代码并且是能直接被机器识别的语言,在运行性能上就更快。Compiler 生成了优化后的代码后会将原先 Interpreter 解释出来的对应代码进行替换,再次执行性能也就提了上来。即然Interpreter 和 Compiler 各有优缺点,那么就需要一个新角色来结合它们的优化进行工作,这个新角色就是JIT(Just in Time)。JIT 的工作之一就是利用 Interpreter 逐行执行的同时,由 Profiler 由寻找可以被优化的代码,再由 Compiler 创建优化的代码。然后将 ByteCode 替换为优化后的代码,也就是机器语言。现如今很多浏览器及 V8 引擎都内置了 JIT 。
(三)小结
如果你觉得上段文字读起来很晦涩,那么你可以简单的理解为:当Chrome浏览器在执行一长串字符串时,先解析成 AST 再将 AST 交给 Interpreter 转成 ByteCode 后交给 Chrome 浏览器内置的 V8 引擎执行;同时 Profiler 查找代码中的优化点并交 Compiler 创建优化后的代码即机器语言或较为底层的语言,并将对应的 ByteCode 代码替换掉。