一、编译器介绍
对于常见编译型语言(例如:Java)来说,编译步骤分为:词法分析->语法分析->语义检查->代码优化和字节码生成。
对于解释型语言(例如 JavaScript)来说,通过词法分析 -> 语法分析 -> 语法树,就可以开始解释执行了。
1 传统的编译步骤:
1、分词/词法分析
将字符组成的字符串分解成有意义的代码块(词法单元)
例: var a = 2 => var、a、=、2
2、解析/语法分析
将词法单元流(数组)转换成抽象语法树
3、代码生成
将 AST 转换为可执行代码的过程
例:用某种方法将 var a = 2 的 AST 转化为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中。
2 Javascript 编译介绍
1、其实 javascript 引擎更复杂,例如在分词/词法分析、代码生成阶段有特定步骤对运行性能、冗余元素进行优化等。
2、首先,JavaScript引擎不会有大量的(像其他语言编译器那么多的)时间用来进行优化,因为与其他语言不同,JavaScript的编译过程不是发生在构建之前的。
3、对于JavaScript来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时间内。在我们所要讨论的作用域背后,JavaScript引擎用尽了各种办法(比如JIT,可以延迟编译甚至实施重编译)来保证性能最佳。
4、简单地说,任何JavaScript代码片段在执行前都要进行编译(通常就在执行前)。因此,JavaScript编译器首先会对var a = 2;这段程序进行编译,然后做好执行它的准备,并且通常马上就会执行它。
3 javascript 的编译过程
分为三个阶段:词法语法分析、预编译阶段、执行阶段
1、词法语法分析阶段:
分词:就是将代码分成原子符号(token)
解析/语法分析:将token解析翻译成 AST(语法生成树)。
如果分析遇到一个语法错误,就会抛出一个语法错误语法错误(syntaxError),然后跳出该 < script /> 代码片段,加载第二个< scrip t/> 代码片段,如果语法正确,则进入到预编译阶段。
2、预编译阶段:
代码生成会将AST转换成可执行的代码。它会在预编译期对所有声明的变量和函数进行处理!并且是先预声明变量,再预定义函数!
a、预编译”可以分为
创建GO对象(global object) 发生在页面加载完成时
创建AO对象(activation object) 发生在函数执行前一刻
b、具体步骤如下:
全局预编译
- 创建GO对象
- 找变量声明,将变量声明作为GO对象的属性名,并赋值undefined
- 找全局里的函数声明,将函数名作为GO对象的属性名,值赋予函数体
局部预编译
- 创建一个AO对象
- 找形参和变量声明,将形参和变量声明作为AO对象的属性名,值为 undefined
- 将实参赋值给形参
- 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体
c、实操:
预编译完后,立马执行。
var a = 100
function foo(){
console.log(a)
}
foo()
编译器:创建一个 GO 对象
- 找到变量声明 var a
- 和函数声明 function foo(){}
- 将上面两个变量声明作为GO的属性名赋初值
GO {
a: undefined
foo: function(){
console.log(a)
}
}
执行(引擎):
- 然后运行第一行代码 a=100
- 在 GO 中将 100 赋值给 a
- 运行 foo 函数
编译器:创建一个AO对象
- 在函数体内找变量声明和形参(无)
- 再在函数体内找函数声明(无)
AO{
}
执行(引擎):
先在AO对象中寻找a的值,发现不存在,向外部作用域扩展,在GO对象中寻找a,发现a的值为100,输出100
二、作用域的辅助工作
1 分工
引擎:从头到尾负责整个 Javascript 程序的编译及执行过程
编译器:负责语法分析及代码生成等
作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行代码对这些标识符的访问权限。
2 编译器和引擎的交互
对于 var a = 2
编译器工作:
a、当遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中;若是,则忽略这步,继续进行编译;否则它会要求作用域在当前作用域集合中声明一个新的变量,并命名为a
b、接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作,引擎运行时会首先询问作用域,在当前作用域集合中是否存在一个叫作 a 的变量。若是,引擎会使用该变量;若否,引擎会继续查找该变量,最终引擎找到则赋值,没找到就会报错。
引擎处理:
编译器在编译过程的第二步中生成了代码,引擎执行它时,会通过查找变量a来判断它是否已声明过,过程中需作用域协助。
3 RHS、LHS查询
变量出现在哪边则执行哪个查询。
RHS (取到他的源值,目的地是获取变量的值)
console.log(a)
LHS(赋值操作的目标是谁?找的目的是对变量赋值)
var a = 2
三、作用域查找和异常报错
1 作用域规则
引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。
2 异常
1、RHS查询:
如果RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError异常。
如果RHS查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用,或着引用null或undefined类型的值中的属性,那么引擎会抛出另外一种类型的异常,叫作TypeError。
2、LHS查询:
引擎执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下。
(严格模式:禁止自动或隐式地创建全局变量。因此,在严格模式中LHS查询失败时,并不会创建并返回一个全局变量,引擎会抛出同RHS查询失败时类似的 ReferenceError 异常。)
总结:ReferenceError同作用域判别失败相关,而TypeError则代表作用域判别成功了,但是对结果的操作是非法或不合理的。
四、引擎和作用域交互过程
编译器已同作用域完成声明工作,下一步编译器为引擎生成运行时所需的代码,用来执行 foo(2) 操作
function foo(a) {
// 2、
// 引擎:需要对 a 进行 LHS查询(需要将 2 赋值给 a, a = 2),在该作用域中查找 a 。
// 作用域:将编译器声明为 foo 的一个形式参数 a,返回给引擎。
// 引擎: 执行 a = 2。
console.log(a)
// 3、
// 引擎对 console 进行 RHS查询,在该作用域中查找 console。
// 作用域:console 是个内置对象,返回给引擎。
// 引擎: 找到 console 里有 log;对 a 进行 RHS 查询,在该作用域里查找 a。
// 作用域:将 a 返回。
// 引擎:将 a的值(也就是2)传到 log(...) 中。
console.log(b)
// 4、
// 省略前面。引擎对 b 进行 RHS查询,在当前作用域中查找 b。
// 找不到,则继续向上到全局作用域中找 b,还是没有,则报错 ReferenceError 异常。
}
foo(2)
// 1、
// 引擎:对 foo 进行 RHS查询,在当前作用域中查找 foo。
// 作用域:将 编译器声明的 foo 返回给引擎
// 引擎:执行 foo。