你不知道的javascript系列-初探js编译机制

1、背景

与其他编译型语言不同,javascript是一种解析型语言。这并不是说解析型语言就没有编译阶段,它运行时会进行两个阶段:编译和执行。那么它的编译过程是怎样的呢?

2.编辑机制

直观上理解javascript是在运行时一行一行代码往下执行的,但实际上并非如此。

考虑下面的例子:

1
2
3
4
// 例子A
a = 1;
var a;
console.log(a);

你认为输出的结果会是什么呢?

有人会认为第一行就会抛出错误,也有人认为最终会输出undefined,但其实上述代码没有问题,最终输出的结果为1.

让我们再看另外一个例子:

1
2
3
// 例子B
console.log(b);
var b = 1;

你认为输出的结果会是什么呢?

有人认为上述代码在第一行就会抛出异常,或者认为上述最终会输出1,但其实上述代码最终输出的结果会是undefined.

由上述例子我们可对js编译阶段有了初步的了解,编译阶段的一部分工作就是找到所有声明,并使用合适的作用域将他们关联起来。关于作用域,前面有介绍过《高性能JavaScript系列-作用域管理原理》

让我们再次回忆一下《编译原理》中的编译过程:

1)词法分析
2)语法分析
3)语义分析和中间代码生成
4)优化
5)目标代码生成

javascript引擎进行编译的步骤与传统编译语言的编译步骤非常相似,在某些步骤中会比预想中要复杂。

在前三个阶段当中,其实作用域已经很明了,包括变量和函数在内的所有声明都会在任何代码执行前首先被处理。这也间接说明为什么不建议使用eval和with等函数。

根据上述编译过程的第三阶段,那么我们回过头分析上述例子A:

1
2
3
4
// 例子A-分析
a = 1; // 赋值,等待执行
var a; // 声明,并被提取到当前作用域最顶端
console.log(a); // 打印,等待执行

那么最终目标代码生成如下:

1
2
3
4
// 例子A-分析
var a;
a = 1;
console.log(a);

同理,可自行分析例子B。

至此,你是不是对js编译机制有了初步的认识?

^……^

接下来,让我们看另外一个例子C:

1
2
3
4
// 例子C
c = 1;
let c;
console.log(c);

ES6新增两个属性let和const,let通常要与var对比理解才更深刻一些。上述例子C与前面提到的例子A非常相似,那么结果是不是一样都输出1呢?答案是否定的,输出的结果是第一行会抛出异常c is not undefined。

两个例子几乎是一致的,编译过程也是一样的,结果却是天壤之别,下面让我们分析一下。

let与var最大的差别是:let声明时会立刻创建一个块作用域,并提取到创建块作用域的顶端;而var并没有块作用域概念,通常充当一个全局作用域的角色,会被提取到当前全局作用域的最顶端。具体详情可参考前面提到的《高性能JavaScript系列-作用域管理原理》

因此,例子C分析如下:

1
2
3
4
// 例子C-分析
c = 1; // 赋值,等待执行
let c; // 声明,创建块作用域,并被提取到当前作用域最顶端
console.log(c); // 打印,等待执行

那么最终目标代码生成如下:

1
2
3
4
5
6
// 例子C-分析
c = 1;
{
let c;
console.log(c);
}

因此,这也是为什么在第一行就抛出c is not undefined的异常错误。

3.总结

1)包括变量和函数在内的所有声明都会在任何代码执行前首先被处理。
2)当变量和函数出现相同名称时,函数声明会被优先提升(var类型变量),与声明的顺序无关。