我们知道JavaScript并不具有动态作用域,它只有词法作用域,什么是词法作用域?
一、 词法作用域
词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。
无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
// 结果是 ???
假设JavaScript采用静态作用域,让我们分析下执行过程:
执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。
假设JavaScript采用动态作用域,让我们分析下执行过程:
执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。
前面我们已经说了,JavaScript采用的是词法作用域,所以这个例子的结果是 1。
二、 but,eval()和with可以通过其特殊性用来“欺骗”词法作用域
欺骗词法
JavaScript 中有两种机制来实现:在运行时来“修 改”(也可以说欺骗)词法作用域,欺骗词法作用域会导致性能下降,所以不要使用eval和with。
(1)eval()
eval() 函数可以接受一个字符串为参数,对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时);
function foo(str, a) {
eval( str ); // 欺骗!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
// 上面代码中,eval()调用中的"var b = 3;"这段代码会被当作本来就在那里一样来处理。
// 由于那段代 码声明了一个新的变量b,因此它对已经存在的foo()的词法作用域进行了修改。
// 这段代码实际上在 foo() 内部创建了一个变量 b,并遮蔽 了外部(全局)作用域中的同名变量。
但在严格模式的程序中,eval()在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域。
function foo(str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2" );
(2)with
with 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。
with 通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(在运行时)。
尽管with 块可以将一个对象处理为词法作用域,但是这个块内部正常的var声明并不会被限制在这个块的作用域中,而是被添加到with 所处的函数作用域中。
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {a: 3};
var o2 = {b: 3};
// 将 o1 传递进去,a=2 赋值操作找到了 o1.a 并将 2 赋值给它
foo( o1 );
console.log( o1.a ); // 2
// 当 o2 传递进去,o2 并没有 a 属性,因此不会创建这个属性, o2.a 保持 undefined
// 此时,a = 2 赋值操作创建了一个全局的变量 a
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!