简单解释一下词法作用域(从书中复制过来的)。
大部分标准语言编译器的第一个工作阶段叫作词法化(也叫单词化)。词法化的过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义。
简单地说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写 代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域 不变(大部分情况下是这样的)。
欺骗词法是在运行时用来“修改(欺骗)”词法作用域的。
JavaScript中有两种机制来实现这个目的,分别是eval(...)和with.但是不建议使用欺骗词法,欺骗词法作用域会导致性能下降。下面我们来介绍一下这两种欺骗词法:
1.eval
JavaScript中的eval()函数可以接受一个字符串为参数,并将这个字符串中的内容视为在写这个程序的时候就存在在这个位置的代码,可以直接使用。
eval()执行以后,引擎不管前面的代码是动态插进来的还是本来存在的。引擎只会像往常一样进行词法作用域查找。
function foo(str, a) {
eval( str ); // 欺骗!相当于在这里写了一句 var b = 2
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
因为执行了eval(str),所以b已经在foo()内部被声明,并遮蔽了外部(全局的)作用于中的同名变量,对词法作用域进行了修改。
当console.log()被执行时,foo()内部已经有a,b两个变量,能够被同时找到,所以就不会再向上一个作用域去寻找,也不会找到外部的变量b了。因此输出的结果是“1,3”,而不是正常情况下的“1,2”。
严格模式
在严格模式的程序中,eval() 在运行时有其自己的词法作用域,意味着其 中的声明无法修改所在的作用域。
function foo(str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2" );
2.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;
}
但实际上这不仅仅是为了方便地访问对象属性。考虑如下代码:
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被泄漏到全局作用域上
当我们传递 o1 给 with 时,with 所声明的作用域是 o1,而这个作用域中含 有一个同 o1.a 属性相符的标识符。但当我们将 o2 作为作用域时,其中并没有 a 标识符, 因此进行了正常的 LHS 标识符查找(查看第 1 章)。 o2 的作用域、foo(..) 的作用域和全局作用域中都没有找到标识符 a,因此当 a=2 执行 时,自动创建了一个全局变量(因为是非严格模式)。
3.性能
eval()和with都会被严格模式所影响(限制)。with 被完全禁止,而在保留核心功能的前提下,间接或非安全地使用 eval(..) 也被禁止了。
这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认 为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。
eval(..) 和 with 会在运行时修改或创建新的作用域,以此来欺骗其他在书写时定义的词 法作用域。
JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。但如果引擎在代码中发现了 eval(..) 或 with,它只能简单地假设关于标识符位置的判断 都是无效的,因为无法在词法分析阶段明确知道 eval(..) 会接收到什么代码,这些代码会如何对作用域进行修改,也无法知道传递给 with 用来创建新词法作用域的对象的内容到底是什么。最悲观的情况就是如果出现了eval()或with,所有的优化可能都是无意义的,因此最简单的做法就是完全不做任何优化。