作用域的概念
现代编程语言的最基本功能之一就是能够存储变量当中的值,以便于之后的使用于修改。也正是这个功能将状态带给了程序。
在JavaScript中,作用域就是一套设计良好的规则来存储变量。
简述编译原理
通常我们会将JavaScript归类为“动态”或“解释执行“语言,但它实际上是一门编译语言。与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。
例如V8引擎,为了提高JavaScript代码的运行性能,在运行之前会先将其编译为本地的机器码,然后再去执行机器码,达到提升速度的目的。
- 分词/词法分析
这个过程将由字符组成的代码分解成对程序有意义的代码块,这些代码块被称为词法单元。
例如 var foo = 'bar' 通常会被分解为这些词法单元:var 、 foo 、 = 、 'bar'
- 解析/语法分析
这个过程将词法单元转换成一个“由元素逐级嵌套组成的代表程序语法的树“,这个树被称为“抽象语法树”(AST)。
image
- 代码生成
将上边的抽象语法树转换为机器可执行代码
JavaScript引擎比只有三个步骤的语言的编译器要复杂的多。例如在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。
对于JavaScript来说,大部分情况下编译发生在代码执行的前几微秒,任何代码片段在执行前都要进行编译。因此JavaScript编译器首先对 var foo = 'bar' 进行编译,然后做好执行它的准备,并且通常马上就会执行它。
引擎、编译器、作用域在赋值操作中的配合
- 引擎:从头到尾负责整个JavaScript程序编译及执行过程
- 编译器:负责语法分析及代码生成
- 作用域:负责收集维护由所有变量组成的一系列查询
对于 var foo = 'bar' 这段代码,大家很有可能认为是一句简单的声明。而事实上JavaScript执行时会将它分成两个完全不同的声明。
- 编译器首先将这段代码分解成词法单元,然后解析为树结构。(在下一步代码生成时,处理这段代码的方式会跟预期有所不同)
- 遇到 var foo ,编译器会检查作用域是否已有同名变量存在。如果有的话编译器会忽略声明,继续编译。否则它会生成代码在当前作用域的变量集合中声明一个新的变量,命名为 foo
- 接下来编译器会为引擎生成运行时所需代码,用来处理 foo = 'bar' 这个赋值操作。
- 引擎运行时会首先查询当前作用域是否存在叫做 foo 的变量。如果有引擎则会使用这个变量,否则会一直向上层作用域查找。
- 如果最终找到了 foo 这个变量,就会将 'bar' 赋给它,否则抛出异常。
总结:变量的赋值会执行两个动作:首先是编译器在当前作用域中声明变量(如果变量未被声明过);接着运行时引擎在作用域查找该变量,能找到就会对它赋值。
LHS查询 vs RHS查询
引擎执行编译器生成的代码时,会通过查找 foo 来判断是否已经声明过。查找的过程由作用域来协助。在我们的例子中,引擎为变量 foo 进行的时LHS查询,还有另一个查找类型叫RHS查询。顾名思义,它们的意思是Left hand side 和 Right hand side
- LHS:变量出现在赋值操作的左侧(查找赋值操作的目标是谁)
- RHS:变量在其他位置出现(查找值的源头)
- // 考虑下边的代码
- console.log(foo)
此例中 foo 的引用就是RHS查询,这里没有赋予 foo 任何值,相反的,我们需要查找 foo 的值,才能传递给log方法。
- // 相比之下
- foo = 'bar'
这里对 foo 的查询则是LHS查询,我们并不关心 foo 当前的值是什么, 只是想为这个赋值操作找到目标。
- // 再分析下边的代码
- function foo(a) {
- console.log(a)
- }
- foo('bar')
这段代码里既有LHS查询又有RHS查询
- 最后一行 foo(...) 函数的调用需要对 foo 进行RHS查询 → 找到 foo 的值
- 入参时存在隐式的 a = 'bar' ,需要对 a 进行LHS查询
- console.log(a) 对 a 进行RHS查询
- console.log(...) 本身也需要对 console 对象进行RHS查询
作用域的嵌套
我们在文章开始时说过,作用域是根据名称查找变量的一套规则。实际情况中需要同时顾及几个作用域。
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此在当前作用域中没有查找到目标变量时,会逐层向上查找直到全局作用域。
- // 考虑以下代码
- function foo(a) {
- console.log(a + b)
- }
- var b = 258;
- foo(369)
对 b 进行的RHS查询无法在 foo 内部完成,但可以在上一级的作用域中完成(在此例中是全局作用域)。
LHS,RHS查询都会在作用域内逐层查找,直到找到为止(或到达全局作用域)。
ReferenceError
上一节提到了LHS,RHS都会在作用域内逐层查找变量,但如果到达全局作用域仍然没有找到变量怎么办呢?
这时区分LHS和RHS查询的意义就体现出来了。
如果RHS查询在所有嵌套的作用域中都没有找到所需变量,引擎就会抛出 ReferenceError。
如果LHS查询在所有嵌套的作用域中都没有找到所需变量,引擎就会在全局作用域中创建一个具有该名称的变量,并将其返回给引擎。
注意:ES5中引入了严格模式,与普通模式相比,严格模式其中一个不同就是进制自动或隐式的创建全局变量。因此在严格模式下LHS查询失败时不会创建并返回全局变量,引擎同样会抛出 ReferenceError。
总结
- 作用域是一套规则,用于确定在何处以及如何查找变量。如果查找的目的是对变量赋值,会使用LHS查询;如果目的是获取变量的值,会使用RHS查询。
- JavaScript引擎会在代码执行前对其进行编译。在这个过程中,像 var foo = 'bar' 这种声明会被分解成两个独立的步骤。
1. var foo 在其作用域中声明新的变量。此操作在代码执行前进行。
2. 接下来 foo = 'bar' 会查询(LHS)变量 foo 并对其赋值。
- LHS和RHS查询都会在当前执行作用域中开始,如果有需要(没有在当前作用域找到变量)就会向上级作用域继续查找目标变量,一直抵达全局作用域,无论找到与否都会停止。
- 不成功的RHS查找会导致抛出 ReferenceError ,不成功的LHS查找会导致自动隐式地创建一个全局变量(非严格模式下),或者抛出 ReferenceError(严格模式下)。