词法作用域

7/15/2020 js

# 作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法 作用域,我们会对这种作用域进行深入讨论。还有一种是动态作用域

# 词法阶段

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

示例:

// 1
function foo(a) { 
    // 2
    var b = a * 2;
    function bar(c) { 
        // 3
        console.log( a, b, c );
    }
    bar( b * 3 ); 
}
foo( 2 ); // 2, 4, 12
1
2
3
4
5
6
7
8
9
10
11
  • // 1 包含着整个全局作用域,其中只有一个标识符:foo。
  • // 2 包含着 foo 所创建的作用域,其中有三个标识符:a、bar 和 b。
  • // 3 包含着 bar 所创建的作用域,其中只有一个标识符:c。

# 查找

作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的 标识符,这叫作"遮蔽效应"(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应, 作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见 第一个匹配的标识符为止

全局变量会自动成为全局对象(比如浏览器中的 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

eval(..) 调用中的 "var b = 3;" 这段代码会被当作本来就在那里一样来处理。**由于那段代 码声明了一个新的变量 b,因此它对已经存在的 foo(..) 的词法作用域进行了修改。**事实 上,和前面提到的原理一样,这段代码实际上在 foo(..) 内部创建了一个变量 b,并遮蔽 了外部(全局)作用域中的同名变量。

JavaScript中 还 有 其 他 一 些 功 能 效 果 和eval(..)很 相 似。setTimeout(..)和 setInterval(..) 的第一个参数可以是字符串,字符串的内容可以被解释为一段动态生成的 函数代码。这些功能已经过时且并不被提倡。不要使用它们! new Function(..) 函数的行为也很类似,最后一个参数可以接受代码字符串,并将其转 化为动态生成的函数(前面的参数是这个新生成的函数的形参)。这种构建函数的语法比 eval(..) 略微安全一些,但也要尽量避免使用

# with

JavaScript 中另一个难以掌握(并且现在也不推荐使用)的用来欺骗词法作用域的功能是 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
13
14
15
16

但实际上这不仅仅是为了方便地访问对象属性。考虑如下代码:

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 赋值给它。

o2 的作用域、foo(..) 的作用域和全局作用域中都没有找到标识符 a,因此当 a=2 执行 时,自动创建了一个全局变量,因为复制操作是符合LHS这种查询的定义

另外一个不推荐使用 eval(..) 和 with 的原因是会被严格模式所影响(限 制)。with 被完全禁止,而在保留核心功能的前提下,间接或非安全地使用 eval(..) 也被禁止了

# 总结

JavaScript 中有两个机制可以“欺骗”词法作用域:eval(..) 和 with。前者可以对一段包 含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在 运行时)。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作 用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时) 这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认 为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。

上次更新: 2/11/2025, 1:00:27 PM