# 第 1 章 作用域是什么

几乎所有编程语言最基本的功能之一,就是能够储存变量当中的值,并且能在之后对这个值进行访问或修改。事实上,正是这种储存和访问变量的值的能力将状态带给了程序。

若没有了状态这个概念,程序虽然也能够执行一些简单的任务,但它会受到高度限制,做不到非常有趣。

但是将变量引入程序会引起几个很有意思的问题,也正是我们将要讨论的:这些变量住在哪里?换句话说,它们储存在哪里?最重要的是,程序需要时如何找到它们

# 1.1 编译原理

尽管通常将 JavaScript 归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。

但与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。

在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编 译”。

  • 词法分析
  • 语法分析
  • 代码生成

# 1.2 理解作用域

当你看见 var a = 2; 这段程序时,很可能认为这是一句声明。但我们的新朋友引擎却不这么看。事实上,引擎认为这里有两个完全不同的声明,一个由编译器在编译时处理,另一个则由引擎在运行时处理。

编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构。但是当编译器开始进行代码生成时,它对这段程序的处理方式会和预期的有所不同。

编译器会进行如下处理。

  1. 遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a。
  2. 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。

如果引擎最终找到了 a 变量,就会将 2 赋值给它。否则引擎就会举手示意并抛出一个异常!

编译器在编译过程的第二步中生成了代码,引擎执行它时,会通过查找变量 a 来判断它是否已声明过。查找的过程由作用域进行协助,但是引擎执行怎样的查找,会影响最终的查找结果

在我们的例子中,引擎会为变量 a 进行 LHS 查询。另外一个查找的类型叫作 RHS。 我打赌你一定能猜到“L”和“R”的含义,它们分别代表左侧和右侧。 什么东西的左侧和右侧?是一个赋值操作的左侧和右侧。

换句话说,当变量出现在赋值操作的左侧时进行 LHS 查询,出现在右侧时进行 RHS 查询。

讲得更准确一点,RHS 查询与简单地查找某个变量的值别无二致,而 LHS 查询则是试图找到变量的容器本身,从而可以对其赋值。

# 1.3 作用域嵌套

我们说过,作用域是根据名称查找变量的一套规则。实际情况中,通常需要同时顾及几个作用域。 当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

# 1.4 异常

ES5 中引入了“严格模式”。同正常模式,或者说宽松 / 懒惰模式相比,严格模式在行为上有很多不同。其中一个不同的行为是严格模式禁止自动或隐式地创建全局变量。因此,在严格模式中 LHS 查询失败时,并不会创建并返回一个全局变量,引擎会抛出同 RHS 查询失败时类似的 ReferenceError 异常。

ReferenceError 同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对结果的操作是非法或不合理的。

# 1.5 小结

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。

不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛出 ReferenceError 异常(严格模式下)