正如我们在第一章中讨论的,标准语言编译器的第一个传统步骤称为词法分析(也就是分词)。如果你回忆一下,词法分析处理是检查一串源代码字符,并给 token 赋予语法含义作为某种有状态解析的输出。

正是这个概念给理解词法作用域是什么提供了基础,它也是这个名字的渊源。

要定义它有点儿兜圈子,词法作用域是在词法分析时被定义的作用域。换句话说,词法作用域是基于,你,在写程序时,变量和作用域的块儿在何处被编写决定的,因此它在词法分析器处理你的代码时(基本上)是固定不变的。

注意: 我们将会稍稍看到有一些方法可以骗过词法作用域,从而在词法分析器处理过后改变它,但是这些方法都是使人皱眉头的。事实上公认的最佳实践是,将词法作用域看作是仅仅依靠词法的,因此自然而然地完全是编写时决定的。

让我们考虑这段代码:

  1. function foo(a) {
  2. var b = a * 2;
  3. function bar(c) {
  4. console.log( a, b, c );
  5. }
  6. bar(b * 3);
  7. }
  8. foo( 2 ); // 2 4 12

在这个代码实例中有三个固有的嵌套作用域。将这些作用域考虑为套在一起的气泡可能有助于思考。

词法分析时 - 图1

气泡1 包围着全局作用域,它里面只有一个标识符:foo

气泡2 包围着作用域 foo,它含有三个标识符:abarb

气泡3 包围着作用域 bar,它里面只包含一个标识符:c

作用域气泡是根据作用域的块儿被写在何处定义的,一个嵌套在另一个内部,等等。在下一章中,我们将讨论作用域的不同单位,但是就现在来说,让我们认为每一个函数创建了一个新的作用域气泡。

bar 的气泡完全被包含在 foo 的气泡中,因为(而且只因为)这就是我们选择定义函数 bar 的位置。

注意这些嵌套的气泡是严格嵌套的。我们没有讨论气泡可以跨越边界的维恩图(Venn diagrams)。换句话说,没有那个函数的气泡可以同时(部分地)存在于另外两个外部的作用域气泡中,就像没有函数可以部分地存在于它的两个父函数中一样。

查询

这些作用域气泡的结构和相对位置完全解释了 引擎 在查找一个标识符时,它需要查看的所有地方。

在上面的代码段中,引擎 执行语句 console.log(..) 并开始查找三个被引用的变量 abc。它首先从最内部的作用域气泡开始,也就是 bar(..) 函数的作用域。在这里它找不到 a,所以它向上走一层,到外面下一个最近的作用域气泡,foo(..) 的作用域。它在这里找到了 a,于是它就使用这个 a。同样的事情也发生在 b 身上。但是对于 c,它在 bar(..) 内部就找到了。

如果在 bar(..) 内部和 foo(..) 内部都有一个 c,那么 console.log(..) 语句将会找到并使用 bar(..) 中的那一个,绝不会到达 foo(..) 中的那一个。

一旦找到第一个匹配,作用域查询就停止了。相同的标识符名称可以在嵌套作用域的多个层中被指定,这称为“遮蔽(shadowing)”(内部的标识符“遮蔽”了外部的标识符)。无论如何遮蔽,作用域查询总是从当前被执行的最内侧的作用域开始,向外/向上不断查找,直到第一个匹配才停止。

注意: 全局变量也自动地是全局对象(在浏览器中是 window,等等)的属性,所以不直接通过全局变量的词法名称,而通过将它作为全局对象的一个属性引用来间接地引用,是可能的。

  1. window.a

这种技术给出了访问全局变量的方法,没有它全局变量将因为被遮蔽而不可访问。然而,被遮蔽的非全局变量是无法访问的。

不管函数是从 哪里 被调用的,也不论它是 如何 被调用的,它的词法作用域是由这个函数被声明的位置 唯一 定义的。

词法作用域查询 仅仅 在处理头等标识符时实施,比如 ab,和 c。如果你在一段代码中拥有一个 foo.bar.baz 的引用,词法作用域查询将在查找 foo 标识符时实施,但一旦定位这个变量,对象属性访问规则将会分别接管 barbaz 属性的解析。