JS编译原理
JavaScript是一门可归类于"动态"或"解释执行"的编程语言,与传统的编程语言不同,它不是提前编译的,编译结果也不能在分布式系统中移植。通常一段源代码在执行之前会经历三个过程:
分词/词法分析
这个过程会将字符串分割为有意义的代码块,这些代码块称之为词法单元。例如变量的声明:
var a = 2;
这行代码会被分为以下词法单元:var、a、=、2(空格算不算词法单元取决于空格对于该编程语言是否具有意义);这些零散的词法单元会组成一个词法单元流(数组)进行解析。
解析/与法分析
这个过程会将词法单元流转换成一棵抽象语法树(Abstract Syntax Tree,AST)在线解析工具。
var a = 2;
的词法单元流就会被解析为下面的AST:
代码生成
将AST转化为可执行的代码。
整个过程看似很简单,但是在语法分析和代码生成阶段有很多坑等着踩。
这里又要引入几个概念了
成员:
1.引擎:负责整个过程中javascript的编译及执行过程。浏览器不同,其引擎也不同,比如Chrome采用的是v8,Safari采用的是SquirrelFish Extreme。
2.编译器:负责语法分析和代码生成。
3.作用域:负责收集并维护所有的标识符(变量)简析JavaScript中的作用域与作用域链
例子分析:
还是对最简单的例子进行分析,var a = 2;
,首先进行词法分析,然后将词法单元流交给编译器生成AST,再有编译器生成可执行的代码。
A.编译器遇到var a;
会询问同一作用域集是否有存在同名的变量,如果有,就忽略该声明,继续编译;如果没有编译器就会要求作用域在当前作用域的集合生命一个新的变量,并命名为a。
B.编译器会为引擎的运行生成一些列代码,这些代码用于为变量a进行赋值操作。引擎会询问当前作用域是否有这个变量的存在,如果有则进行赋值操作,如果没有就开始查找这个变量(从当前作用域向上查找,直到全局作用域,如果还是没有,就会抛出一个异常)。
C.LHS和RHS,当引擎执行编译器给的代码(赋值操作)时,会通过查找这个变量来判断这个变量是否已经声明,这个过程需要作用域的协助,而查找的方式分为两种:LHS(“赋值操作的目标是谁”)、RHS(”谁是赋值操作的源头“)。例如下面这个例子:
var a; //RHS引用
a = 2; //LHS引用
alert(a); //RHS引用
/*这段代码块既有RHS引用也有LHS引用,
foo(2),2被当作函数参数传递给foo()时,2会被分配给变量a(a = 2);
*/
function foo(a){
alert(a);
}
foo(2);
区分RHS和LHS也很重要,尤其分析异常时。例如下面这个例子:
function foo(a){
alert(a + b);
b = a;
}
foo(2);
第一次对b进行RHS查询会查询不到这个变量,因为它是一个未声明的变量,在所有作用域都无法找到(var b;);此时引擎会抛出一个异常(ReferenceError)。在非严格模式下,当引擎进行LHS查询查询不到某个变量时,全局作用域会创建一个同名的变量交给引擎,当然这个变量具有全局作用域;而在严格模式下,引擎会抛出ReferenceError的异常。
提升
变量和函数在内的声明都在任何代码执行前被处理。声明操作在编译阶段时进行的,而赋值操作是在等到执行阶段才执行。
//代码块1
var a = 2;
alert(a); // 输出2
//代码块2
b = 2;
var b;
alert(b); //输出2
//代码块3
alert(c); //输出undefined
var c = 2;
//代码块4
var d;
alert(d); //输出undefined
d = 2;
代码块2,4等价于代码块1,3(除了变量名不同,内存地址不同);这个过程就好像变量和函数声明的代码被移动到了最上面,这个过程就叫提升。
函数声明可以提升,函数表达式不能提升。
//函数声明可以提升
foo(); //输出2;
function foo(){
alert(2);
//函数表达式不可提升
bar(); //TypeError
var bar = function f1(){
alert(2);
}
函数声明优先于变量声明提升,出现在后面的函数声明可以覆盖之前的声明。
foo(); //输出3
function foo(){
alert(1);
}
var foo = function bar(){
alert(2);
}
function foo(){
alert(3);
}