词法作用域

# 词法作用域

简单地说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写 代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域 不变

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

全局变量会自动成为全局对象(比如浏览器中的 window 对象)的属性,因此 可以不直接通过全局对象的词法名称,而是间接地通过对全局对象属性的引 用来对其进行访问。

无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处 的位置决定。

# 欺骗词法

如果词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来“修 改”(也可以说欺骗)词法作用域呢?

欺骗词法作用域会导致性能 下降。

# eval

在执行 eval(..) 之后的代码时,引擎并不“知道”或“在意”前面的代码是以动态形式插 入进来,并对词法作用域的环境进行修改的。引擎只会如往常地进行词法作用域查找

function foo(str, a) { 
 eval( str ); // 欺骗! 
 console.log( a, b ); 
}
var b = 2; 
foo( "var b = 3;", 1 ); // 1, 3
1
2
3
4
5
6
  • eval(..) 调用中的 "var b = 3;" 这段代码会被当作本来就在那里一样来处理。
  • 它对已经存在的 foo(..) 的词法作用域进行了修改
  • 这段代码实际上在 foo(..) 内部创建了一个变量 b,并遮蔽了外部(全局)作用域中的同名变量。

默认情况下,如果 eval(..) 中所执行的代码包含有一个或多个声明(无论是变量还是函 数),就会对 eval(..) 所处的词法作用域进行修改。技术上,通过一些技巧(已经超出我 们的讨论范围)可以间接调用 eval(..) 来使其运行在全局作用域中,并对全局作用域进行 修改。但无论何种情况,eval(..) 都可以在运行期修改书写期的词法作用域。

在严格模式的程序中,eval(..) 在运行时有其自己的词法作用域,意味着其 中的声明无法修改所在的作用域。

相 似

setTimeout(..) 和 setInterval(..) 的第一个参数可以是字符串,字符串的内容可以被解释为一段动态生成的 函数代码。

new Function(..) 函数的行为也很类似,最后一个参数可以接受代码字符串,并将其转 化为动态生成的函数

不被提倡。不要使用它们!

# with

它如何同 被它所影响的词法作用域进行交互

用来欺骗词法作用域的功能是 with 关键字。

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

var obj = { a: 1, b: 2, c: 3 };// 单调乏味的重复 "obj" 

obj.a = 2; 
obj.b = 3; 
obj.c = 4; 

// 简单的快捷方式
with (obj) { 
 a = 3; 
 b = 4; 
 c = 5; 
}
1
2
3
4
5
6
7
8
9
10
11
12
function foo(obj) {
 with (obj) { 
  a = 2; 
 } 
}

var o1 = { a: 3 };
var o2 = { b: 3 };

foo( o1 ); 
console.log( o1.a ); // 2 

foo( o2 ); 
console.log( o2.a ); // undefined 
console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在 with 块内部,我们写的代码看起来只是对变量 a 进行简单的词法引用,实际上就是一个 LHS 引用,并将 2 赋值给它。

with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对 象的属性也会被处理为定义在这个作用域中的词法标识符。

尽管 with 块可以将一个对象处理为词法作用域,但是这个块内部正常的 var 声明并不会被限制在这个块的作用域中,而是被添加到 with 所处的函数作 用域中。

(因为是非严格模式)。

eval(..) 函数如果接受了含有一个或多个声明的代码,就会修改其所处的词法作用域,而 with 声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域

另外一个不推荐使用 eval(..) 和 with 的原因是会被严格模式所影响

# 性能

JavaScript 引擎会在编译阶段进行数项的性能优化。

其中有些优化依赖于能够根据代码的 词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到 标识符

eval(..) 或 with,它只能简单地假设关于标识符位置的判断 都是无效的,因为无法在词法分析阶段明确知道 eval(..) 会接收到什么代码,这些代码会 如何对作用域进行修改,也无法知道传递给 with 用来创建新词法作用域的对象的内容到底 是什么。

最悲观的情况是如果出现了 eval(..) 或 with,所有的优化可能都是无意义的,因此最简 单的做法就是完全不做任何优化。

如果代码中大量使用 eval(..) 或 with,那么运行起来一定会变得非常慢。

# 小结

  • 词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。
  • 编译的词法分析阶段 基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它 们进行查找。

eval(..) 和 with。

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

这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认 为这样的优化是无效的。

上次更新: 2022/7/21 上午10:29:33