编译原理
在传统编译语言流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。
分词/词法分析
将由字符组成的字符串分解为(对编程语言来说)有意义的代码块,这些代码块被称为此法单元。如将var a = 2
分解为var
、a
、=
、2
、;
。
解析/词法分析
将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树,称为“抽象语法树”(Abstract Syntax Tree,AST)。
代码生成
将 AST 转换可执行代码的过程。简单来说就是将 AST 转化为一组机器指令。
任何 JS 代码片段在执行前都要进行编译(通常就在执行前,所需时间很短)。
理解作用域
演员表
引擎
从头到尾负责整个 JavaScript 程序的编译及执行过程。
编译器
负责语法分析及代码生成等脏活累活。
作用域
负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前代码对这些标识符的访问权限。
处理过程
我们来分析一下对于var a = 2
,引擎和它的朋友们是如何协同工作的。
- 遇到
var a
,编译器会询问作用域是否存在一个名称为 a 的变量在同一作用域中。如果是,编译器会忽略该声明,继续编译;如果否,就让作用域在当前作用域声明一个新的变量 a。 - 编译器为引擎生成运行时所需的代码。引擎询问作用域在当前作用域中是否存在变量 a,如果存在使用这个变量;如果不存在,引擎会继续查找该变量。
- 如果引擎最后找到变量 a,就会把 2 赋值给它;否则,就抛出一个异常。
查询变量的方式
- LHS 查询 : 变量出现在赋值操作左侧时进行 LHS 查询,给函数参数赋值也是一种隐式 LHS 查询。
- RHS 查询:变量出现在赋值操作右侧时进行 RHS 查询,可以理解为“得到某某的值”,比如,console.log(a)、foo(2),都进行了 RHS 查询,查询了变量 a 和 foo 函数。
函数声明不是 LHS 查询和赋值,函数名和函数之间的关联是在作用域的开头自动设置的
作用域嵌套
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。在当前作用域无法找到某个变量时,引擎会在外层嵌套的作用域中继续查找,直到找到该变量或到最外层作用域为止。
异常
在进行 RHS 查询时,如果在全局作用域中也无法找到目标变量,引擎就会抛出ReferenceError
异常
LHS 查询失败时,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎(非严格模式下)
严格模式下,LHS 查询失败同样会抛出ReferenceError
异常。
如果 RHS 查询成功,但是对这个变量进行不合理的操作时,比如引用null
或undefined
的属性时,那么引擎就会抛出TypeError
异常。