这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
什么是作用域
编译原理
程序中的一段源代码在执行前会经历三个步骤,统称为“编译”。
- 分词/词法分析(Tokenizing/Lexing)
这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元。例如,考虑程序var a = 2;这段程序通常会被分解成为下面这些词法单元:var、a、=、2、;。
- 解析/语法分析(Parsing)
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树“(Abstract Syntax Tree,AST)。
var a = 2;的抽象语法树中可能会有一个叫作VariableDeclaration的顶级节点,接下来是一个叫作Identifier(它的值是a)的子节点,以及一个叫作AssignmentExpression的子节点。AssignmentExpression节点有一个叫作NumericLiteral(它的值是2)的子节点。
- 代码生成
将AST转换为可执行代码的过程被称为代码生成。简单来说就是有某种方法可以将var a = 2;的AST转化为一组机器指令,用来创建一个叫作a的变量(包括分配内存等),并将一个值存储在a中。
比起编译阶段只有三个步骤的语言的编译器,JavaScript引擎要复杂得多。例如,语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。JavaScript引擎用尽了各种办法(比如JIT,可以延迟编译甚至实施重编译)来保证性能最佳。
理解作用域
我们想要理解作用域,首先先理解程序是如何进行处理的。
- 引擎,从头到尾负责整个JavaScript程序的编译及执行过程。
- 编译器,负责语法分析及代码生成。
- 作用域,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
我们将上诉三者的交互当作一场对话。
对话
对于var a = 2;这段程序,引擎认为这里有两个不同的声明,一个由编译器在编译时处理,另一个则由引擎在运行时处理。
编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个数结构。当编译器开始进行代码生成时,编译器会进行如下处理:
- 遇到
var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a。 - 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理
a = 2这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。
如果引擎找到了a变量,就会进行赋值操作,否则引擎会抛出一个异常。
总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
编译器说
编译器在编译过程中生成代码,引擎执行它时,会通过查找变量a来判断它是否已声明过。查找的过程由作用域进行协助,但是引擎执行怎样的查找,会影响最终的查找结果。
在上面这个例子中,引擎会为变量a进行LHS查询,相应的还有另外一种查询类型RHS。
对于这两种查询类型,可以理解为“赋值操作的目标是谁(LHS)”及“谁是赋值操作的源头(RHS)”。
引擎和作用域的对话
引擎和作用域会根据顺序将代码进行处理,包括LHS引用及RHS引用等。
作用域嵌套
作用域是根据名称查找变量的一套规则。
当一个块或函数嵌套在另一个块或函数中,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。
遍历嵌套作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没有找到,查找过程都会停止。(LHS和RHS都会当前作用域查找,如果没有找到,就往上一层...)
小结
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。赋值操作符会导致LHS查询。=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。
JavaScript引擎首先会在代码执行前对其进行编译,在这个过程中,像var a = 2这样的声明会被分解为两个独立的步骤:
- 首先,var a在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。
- 接下来,a = 2会查询(LHS查询)变量a并对其进行赋值。
LHS和RHS查询都会在当前作用域中开始,如果有需要(也就是说它们没有找到所需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域,最后抵达全局作用域,无论找到或没找到都将停止。
不成功的RHS引用会导致抛出ReferenceError异常。不成功的LHS引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用LHS引用的目标作为标识符,或者抛出ReferenceError异常(严格模式下)。