作用域-词法作用域
定义
- 词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,或者说 词法作用域完全由写代码期间函数所声明的位置来定义,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。
- 无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。
- 词法作用域查找只会查找一级标识符 词法阶段 大部分标准语言编译器的第一个工作阶段叫作词法化(也叫单词化)。词法化的过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义。
- 查找阶段: 在上一个代码片段中,引擎执行 console.log(..) 声明,并查找 a、b 和 c 三个变量的引用。它首先从最内部的作用域,也就是 bar(..) 函数的作用域气泡开始查找。引擎无法在这里找到 a,因此会去上一级到所嵌套的 foo(..) 的作用域中继续查找。在这里找到了 a,因此引擎使用了这个引用。对 b 来讲也是一样的。而对 c 来说,引擎在 bar(..) 中就找到了它。如果 a、c 都存在于 bar(..) 和 foo(..) 的内部,console.log(..) 就可以直接使用 bar(..)中的变量,而无需到外面的 foo(..) 中查找。
- 作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。
- 词法作用域图

- 注意: 没有任何函数的气泡可以(部分地)同时出现在两个外部作用域的气泡中,就如同没有任何函数可以部分地同时出现在两个父级函数中一样。
欺骗词法
两种机制改变词法作用域
eval()
- 可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)。
with()
- with()本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。 弊端:这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。
总结
- 词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。
- JavaScript 中有两个机制可以“欺骗”词法作用域:eval(..) 和 with。前者可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。
- 这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。