词法作用域

作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法 作用域,我们会对这种作用域进行深入讨论。另外一种叫作动态作用域,仍有一些编程语 言在使用(比如 Bash 脚本、Perl 中的一些模式等)。

词法阶段

词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。

考虑一下代码:

1
2
3
4
5
6
7
8
9
10
11
function foo(a) {
var b = a * 2

function bar (c) {
console.log(a, b, c)
}

bar(b * 3)
}

foo(2)

在这个例子中有三个逐级嵌套的作用域,为了帮助理解,可以将他们想象成几个逐级包含的气泡。

  • 包含整个全局作用域,其中只有一个标识符: foo
  • 包含着foo所创建的作用域,其中有三个标识符: a、bar和b
  • 包含着由bar所常见的作用域,其中只有一个标识符: c

下一章会讨论不同类型的作用域,但现在只是简单的假设没一个函数都会创建一个新的作用域气泡
bar的气泡位置被完全包含在foo所创建的气泡中,唯一的原因就是我们希望定义函数bar的位置

查找

作用域气泡的结构和相互位置关系给引擎提供了足够的位置信息,引擎用这些信息来查找标识符的位置。f引擎会从最内部的作用域中查找,如果没有找到会依次沿着上一级所嵌套的作用域中查找。

作用域查找会在找到第一个匹配的标识符时停止。

多层的嵌套作用域中可以定义同名的 标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。

欺骗词法

量使用 eval(..) 或 with,那么运行起来一定会变得非常慢。无论引擎多聪 明,试图将这些悲观情况的副作用限制在最小范围内,也无法避免如果没有这些优化,代 码会运行得更慢这个事实。

eval

JavaScript 中的 eval(..) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书 写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并 运行,就好像代码是写在那个位置的一样。

with

with 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象 本身。

小结

JavaScript 中有两个机制可以“欺骗”词法作用域:eval(..) 和 with。前者可以对一段包 含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在 运行时)。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作 用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。

这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认 为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。