上文我们将“作用域”定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。而作用域共有两种主要的工作模型,一种是最为普遍的,被大多数编程语言所采用的词法作用域,另一种是动态作用域。
词法作用域与词法阶段
词法作用域是定义在词法阶段的作用域,那么什么是词法阶段呢?
大部分标准语言编译器的第一个工作阶段叫做词法化(也叫单词化)。词法化的过程会对源代码中的字符进行检查,如果是由状态的解析过程,还会赋予单词语义。
换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里决定的,因此当词法分析器处理代码时会保持作用域不变。
我们以下面的代码举例:
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c);
}
bar(b * 3);
}
foo(2);
// 2, 4, 12
这个例子中有三个逐级嵌套的作用域,分别是:
- 包含着整个全局的作用域,其中只有一个标识符:foo
- 包含着 foo 函数所创建的作用域,其中有三个标识符:a、b、bar。
- 包含着 bar 函数所创建的作用域,其中只有一个标识符:c。
其中 bar 的作用域被 foo 作用域完全包裹,所以引擎在查找 bar 里的a、b 标识符时,不止停留在 bar 作用域内查找,还向上到 foo 作用域中查找,在 foo 作用域中找到了 a 和 b 的引用,因此引擎使用了这个引用。
作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫做“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者向上进行,直到遇见第一个匹配的标识符为止。
JavaScript 中的全局变量会自动成为全局对象 window 的属性,因此可以不直接通过全局对象的词法名称,而是间接地通过对全局对象属性的引用来对其进行访问。想要访问全局变量 a,可以通过window.a的形式来绕过被同名变量遮蔽的全局变量。
无论要查找的对象层级多深,词法作用域查找只会查找一级标识符,比如a、b、c,如果引用了多层级的变量:foo.bar.baz,词法作用域只会试图查找 foo 标识符,找到这个变量后,对象属性访问规则会分别接管对 bar 和 baz 属性的访问。
JavaScript中有两个机制可以“欺骗”词法作用域:eval(..)和with。前者可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。
这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们