正如我们在第一章中讨论的,标准语言编译器的第一个传统步骤称为词法分析(也就是分词)。如果你回忆一下,词法分析处理是检查一串源代码字符,并给 token 赋予语法含义作为某种有状态解析的输出。
正是这个概念给理解词法作用域是什么提供了基础,它也是这个名字的渊源。
要定义它有点儿兜圈子,词法作用域是在词法分析时被定义的作用域。换句话说,词法作用域是基于,你,在写程序时,变量和作用域的块儿在何处被编写决定的,因此它在词法分析器处理你的代码时(基本上)是固定不变的。
注意: 我们将会稍稍看到有一些方法可以骗过词法作用域,从而在词法分析器处理过后改变它,但是这些方法都是使人皱眉头的。事实上公认的最佳实践是,将词法作用域看作是仅仅依靠词法的,因此自然而然地完全是编写时决定的。
让我们考虑这段代码:
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar(b * 3);
}
foo( 2 ); // 2 4 12
在这个代码实例中有三个固有的嵌套作用域。将这些作用域考虑为套在一起的气泡可能有助于思考。
气泡1 包围着全局作用域,它里面只有一个标识符:foo
。
气泡2 包围着作用域 foo
,它含有三个标识符:a
,bar
和 b
。
气泡3 包围着作用域 bar
,它里面只包含一个标识符:c
。
作用域气泡是根据作用域的块儿被写在何处定义的,一个嵌套在另一个内部,等等。在下一章中,我们将讨论作用域的不同单位,但是就现在来说,让我们认为每一个函数创建了一个新的作用域气泡。
bar
的气泡完全被包含在 foo
的气泡中,因为(而且只因为)这就是我们选择定义函数 bar
的位置。
注意这些嵌套的气泡是严格嵌套的。我们没有讨论气泡可以跨越边界的维恩图(Venn diagrams)。换句话说,没有那个函数的气泡可以同时(部分地)存在于另外两个外部的作用域气泡中,就像没有函数可以部分地存在于它的两个父函数中一样。
查询
这些作用域气泡的结构和相对位置完全解释了 引擎 在查找一个标识符时,它需要查看的所有地方。
在上面的代码段中,引擎 执行语句 console.log(..)
并开始查找三个被引用的变量 a
,b
和 c
。它首先从最内部的作用域气泡开始,也就是 bar(..)
函数的作用域。在这里它找不到 a
,所以它向上走一层,到外面下一个最近的作用域气泡,foo(..)
的作用域。它在这里找到了 a
,于是它就使用这个 a
。同样的事情也发生在 b
身上。但是对于 c
,它在 bar(..)
内部就找到了。
如果在 bar(..)
内部和 foo(..)
内部都有一个 c
,那么 console.log(..)
语句将会找到并使用 bar(..)
中的那一个,绝不会到达 foo(..)
中的那一个。
一旦找到第一个匹配,作用域查询就停止了。相同的标识符名称可以在嵌套作用域的多个层中被指定,这称为“遮蔽(shadowing)”(内部的标识符“遮蔽”了外部的标识符)。无论如何遮蔽,作用域查询总是从当前被执行的最内侧的作用域开始,向外/向上不断查找,直到第一个匹配才停止。
注意: 全局变量也自动地是全局对象(在浏览器中是 window
,等等)的属性,所以不直接通过全局变量的词法名称,而通过将它作为全局对象的一个属性引用来间接地引用,是可能的。
window.a
这种技术给出了访问全局变量的方法,没有它全局变量将因为被遮蔽而不可访问。然而,被遮蔽的非全局变量是无法访问的。
不管函数是从 哪里 被调用的,也不论它是 如何 被调用的,它的词法作用域是由这个函数被声明的位置 唯一 定义的。
词法作用域查询 仅仅 在处理头等标识符时实施,比如 a
,b
,和 c
。如果你在一段代码中拥有一个 foo.bar.baz
的引用,词法作用域查询将在查找 foo
标识符时实施,但一旦定位这个变量,对象属性访问规则将会分别接管 bar
和 baz
属性的解析。