eval与with
除了eval与with的基本用法外,本文着重强调的是通过eval与with欺骗作用域
eval(属于window)
- eval的基本用法
JavaScript 中的eval(..) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码
例如:
eval('var a = 100;');
function test(){
eval('var b = 200;');
console.log(a + ',' + b);//100,200
}
test();
- 通过eval欺骗词法作用域
思考以下代码:
function foo(str, a) {
eval( str ); // 欺骗!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
在执行eval(..) 之后的代码时,引擎并不“知道”或“在意”前面的代码是以动态形式插入进来,并对词法作用域的环境进行修改的。引擎只会如往常地进行词法作用域查找。
eval(..) 调用中的”var b = 3;” 这段代码会被当作本来就在那里一样来处理。由于那段代码声明了一个新的变量b,因此它对已经存在的foo(..) 的词法作用域进行了修改。事实上,和前面提到的原理一样,这段代码实际上在foo(..) 内部创建了一个变量b,并遮蔽了外部(全局)作用域中的同名变量。当console.log(..) 被执行时,会在foo(..) 的内部同时找到a 和b,但是永远也无法找到外部的b。因此会输出“1, 3”而不是正常情况下会输出的“1, 2”
但是在严格模式下,eval无法修改作用域
如:
var a = 10;
function test(){
"use strict";
eval('var a = 20;');
console.log(a);
}
test();//10
with关键字
当然,with本身就是用来改变作用域的
with会根据你传递给它的对象凭空创建一个全新的词法作用域。而这个对象的属性也会被处理为定义在这个作用域中的词法标识符(就相当于对象中的属性似乎变为了with生成的作用域中的变量一样)
考虑下列代码:
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 被泄漏到全局作用域上了
上述代码分析如下:
foo(..) 函数接受一个obj参数,该参数是一个对象引用,并对这个对象引用执行了with(obj){..}。
然后with生成一个词法作用域,o1中的属性a就相当于定义在with作用域中的变量
执行a=2,此时对a进行LHS查询,注意,这儿是首先到with形成的作用域中查询(至于为什么,请看后面的词法作用域包含分析)。在with中查询到a,并给a赋值2,此时a原来的值3被覆盖,现在值为2。所以
o1.a输出2
foo(..) 函数再次接受o2参数,该参数是一个对象引用,并对这个对象引用执行了with(obj){..}。
然后with生成一个词法作用域,o2中的属性b就相当于定义在with作用域中的变量
执行a=2,此时对a进行LHS查询,注意,这儿是首先到with形成的作用域中查询(至于为什么,请看后面的词法作用域包含分析)。在with中只有b,查询a失败(注意这而并不会在o2中创建一个a属性。因为这儿a=2不同于o2.a=2)
根据LHS查询规则,继续到上一层词法作用域(这儿是foo函数作用域)中查找,依然查询失败
在上一层(即全局)中查找,依然失败,根据LHS查询规则,会自动在全局作用域中创建一个变量a(非严格模式下)并赋值为2
固o2.a为undefined,全局a为2
对上述代码的词法作用域包含分析:
上述代码的词法作用域是这样嵌套的(由内向外):with词法作用域–>函数foo词法作用域–>全局作用域
尽管with 块可以将一个对象处理为词法作用域,但是这个块内部正常的var声明(只是变量声明,不包含赋值)并不会被限制在这个块的作用域中,而是被添加到with 所处的函数作用域中。(with中的函数声明也会被添加到with所处的函数作用域中)
小例题:
function test(){
with(obj){
var b = 100;//b被提到test中声明,但在with中赋值
console.log(b);//100
}
console.log(b);//undefined
}
var obj = {
b : 222
}
test();
console.log(obj.b);//100
console.log(b);//报错
eval与with对性能的影响
JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。但如果引擎在代码中发现了eval(..) 或with,它只能简单地假设关于标识符位置的判断都是无效的,因为无法在词法分析阶段明确知道eval(..) 会接收到什么代码,这些代码会如何对作用域进行修改,也无法知道传递给with用来创建新词法作用域的对象的内容到底是什么。
ps:本文参考并引用下列书籍
《你不知道的JavaScript》(上卷)