# JS 编译器,解释引擎
这一章来巩固一下 js 底层知识,编译器和解释引擎。
- 编译过程
- 实战分析
- JIT 优化
# 编译过程
- 词法分析
- 语法分析
- 代码生成
- 预编译
- 解释器引擎执行
接下来的案例均根据以下代码进行。
var a = 10;
# 词法分析
把输入的字符串分解为一些对编程语言有意义的代码块(词法单元)。
例如上文案例会被解析成 var
,a
,=
,10
。
# 语法分析
这个过程会将词法单元转换成一棵抽象语法树(Abstract Syntax Tree,AST)。
# 代码生成
将 AST 代码转换为可执行代码。简单来说,就是将 AST 转化为一组机器指令
,用来创建一个叫做 a 的变量(包括分配内存等),并将一个值 10 存储在 a 中。
# 预编译
在引擎执行代码之前,在当前作用域下,会把带有 var 和 function 关键字的事先声明,并保存在当前作用域内存中(这也就是变量声明提升和函数声明提升的原因)。
变量和函数在内的声明都在任何代码执行前被处理。
函数声明优先于变量声明提升,出现在后面的函数声明可以覆盖之前的声明。
在预编译阶段会执行以下操作:
- 在编译器,遇到 var a 时,会查找同一作用域是否存在同名变量。
- 如果存在,则忽略该变量声明。
- 如果不存在,则会在
当前作用域中
创建一个 a 变量,
# 解释器引擎执行
解释器引擎,常用的就是 Chrome V8,和 Firefox SpiderMonkey 引擎。
- 在引擎执行 a = 10 代码时,会查找同一作用域是否存在 a 变量。
- 如果存在,则进行赋值操作。
- 如果不存在,则会在上层作用域中去寻找 a。
- 如果都找不到 a,那就在全局作用域去创建 a,并赋值(严格模式会报错)。
# 作用域
负责收集并维护所有的标识符(变量)。在 ES6 以前没有块级作用域,只有函数级作用域,但 catch 模块除外。
在编译器编译变量声明时,或者解释器引擎使用变量时,有一个 LHS 和 RHS 概念。
- LHS 可以理解为变量赋值等号左边的部分,用于查询变量是否声明。
- RHS 可以理解为变量赋值等号右边的部分,用于查询变量的值。
# 实战分析
接下来,我们根据以下代码进行 js 编译流程分析。
var a;
a = 2;
console.log(a);
2
3
1、编译器解析 var a,执行 LHS 查询,询问作用域,当前作用域有 a 吗?
2、作用域回复,没有。
3、于是编译器就在当前作用域上,创建了一个 a。
4、编译器执行到 a = 2,生成机器代码,交给引擎执行。
5、解释引擎开始执行 a = 2,首先进行 LHS 查询,询问作用域,当前作用域有 a 吗?
6、作用域回复,有,因为已经被编译器创建了。
7、执行赋值操作(如果没有,则会继续询问上层作用域)。
至此 var a ; a = 2; 执行完毕。
8、引擎执行 console.log(a); 执行 RHS 查询,询问作用域,当前作用域有 console 吗?
9、作用域回复,没有,但在全局作用域找到了。
10、解释引擎继续执行 console.log(a) ,首先执行 RHS 查询,询问作用域,当前作用域有 a 吗?
11、作用域回复,有,并返回 a。
12、引擎执行 console.log(a) 打印操作。
# JIT 引擎优化
JS 是解释型语言且弱类型,在生成 AST 之后,就开始一边编译,一边解释,但是有个弊端,当某段代码被多次执行时,就会进行多次编译解析操作,特别是在一个循环内,将消耗大量的性能。
JIT 是编译型语言和解释性语言的混合,在一边编译,一边解释时做了一些优化,常用的优化有以下几点。
1、监视器在 js 引擎中增加一个监视器。监视器监控着代码的运行情况,记录代码一共运行了多少次、如何运行的等信息,如果同一行代码运行了几次,这个代码段就被标记成了 warm
,如果运行了很多次,则被标记成 hot
。
2、基线编译器如果一段代码变成了 warm
,那么 JIT 就把它送到基线编译器去编译,并且把编译结果存储起来。比如:监视器监视到了某行、某个变量执行同样的代码、使用了同样的变量类型,就会把编译后的版本,替换这一行代码的执行,并且存储。
3、优化编译器如果一个代码段变得 hot
,监视器会把它发送到优化编译器中。生成一个更快速和高效的代码版本出来,并且存储。例如:循环加一个对象属性时,假设它是 INT 类型,优先做 INT 类型的判断。
4、去优化可是对于 JS 从来就没有确定这么一说,前 99 个对象属性保持着 INT 类型,可能第 100 个就没有这个属性了,那么这时候 JIT 会认为做了一个错误的假设,并且把优化代码丢掉,执行过程将会回到解释器或者基线编译器,这一过程叫做去优化。
# JIT 案例
var a;
for (var i = 0; i < 10; i++) {
if (i > 5) {
a = "a";
} else {
a += i;
}
}
2
3
4
5
6
7
8
如果 a 和 i 一直都是 number 类型,但 js 是弱类型,在执行时,还是会先判断左右两边的类型,这样判断下去很费时,于是 JIT 做了以下两点优化:
增加一个监视器,监视每一步代码的执行情况。
1、将 warm 代码加入基线编译器,如果监视器监视到执行同样的代码和同样的类型,就会直接复用已编译的版本。
2、将 hot 代码加入优化编译器,为了更快的执行,编译器要会做假设,即将 a 和 i 的类型假设为 number,并缓存起来,下一次直接执行 number 类型相加。
3、如果 hot 代码中的假设不成立了,如 i > 5 之后,i 就变成了一个 string 类型了,这时候就执行(去优化)操作,重置到基线编译结果。
什么时候会进行编译优化呢?
在一行代码多次执行时,会被标记 warm,被更多次执行时,会被标记 hot。warm 会被加入到基线编译器,hot 会被加入到优化编译器。