之前在祼看ECMA262-5,在说到eval
的地方,死活看不明白为什么会有一节专门扯到Direct Call to Eval
:
A direct call to the eval function is one that is expressed as a CallExpression that meets the following two conditions:
The Reference that is the result of evaluating the MemberExpression in the CallExpression has an environment record as its base value and its reference name is "eval".
The result of calling the abstract operation GetValue with that Reference as the argument is the standard built-in function defined in 15.1.2.1.
当时觉得写规范的那群家伙一定是有毛病,众所周知,eval无非是接收一个字符串,把这个字符串当做代码解释执行。
这个问题我一直不明白,然后就放着。
直到有一天.....
关于闭包
某一天,小伙伴们讨论到说关于闭包变量的问题时,Y君指出,如果一个函数没有引用到其所处闭包的a
变量,那这个a
变量所指向的空间将被释放。
function getClosure() {
var a = 1;
var b = 2;
return function inner() {
debugger;
console.log(b);
}
}
var func = getClosure();
func();
正如上面代码所示,当打开Chrome调试工具时到达debugger
语句时,我们只能访问到b
变量。试图访问a
是会抛出ReferenceError的。
但是这真的能证明a
引用已经被释放了吗?如果没被释放的话,还不让在调试的时候访问,这个设计就只能说是(哔~)坑爹了,Y君补充道。
于是我写了如下代码做了测试:
function getClosure() {
var a = 1;
var b = 2;
return function inner(val) {
debugger;
console.log(eval(val));
}
}
var func = getClosure();
func('a');
func('b');
在闭包中使用eval
,这样引擎就不知道我在闭包中的会引用到什么变量了。这一次到达debugger
语句时,惊奇地发现,a
和b
都可以直接引用到了。难道是因为eval
的原因?引擎在发现闭包中有eval
之后,就不会回收闭包中的垃圾了?(因为它无从得知哪些变量会在将来被引用到)。
但如果引擎不知道将来会不会执行到eval呢?
function getClosure(){
var a = 1;
var b = 2;
return function inner(func, val) {
debugger;
console.log(func(val));
}
}
var f = getClosure();
f(eval, a);
把eval
当做函数传进去,然后让这个eval函数解释执行传入的val指向的变量。遗憾的是,到在debugger处,无法访问到a和b,执行到console.log(func(val))
抛了异常,表示找不到引用。
于是我才回过神,似乎之前阅读过蛇精病般的什么direct call to eval
的东西,跟这个相关?
回到eval
在规范中指出,进调用eval函数时:
- 如果是直接调用eval函数的话,将当前的
this指向
,词法环境
以及变量环境
当做新的执行上下文的this指向
,词法环境
以及变量环境
。 - 如果不是直接调用的话,则新的执行上下文就相当于全局执行上下文。
>更准确地说,则是以global对象
,全局词法环境
以及全局变量环境
当做新的执行上下文的this指向
,词法环境
以及变量环境
。
那什么是直接调用呢?参照篇首。简言之,有如下限制:
a. BaseValue必须是一个Environment Record
b. Reference Name必须是"eval"
如下例子:
var a = eval;
a('hello'); //非直接调用,因为Reference Name是'a'而不是'eval'
var f = {eval: eval};
f.eval("hello"); //非直接调用,因为BaseValue是f变量,而不是Environment Record
eval('hello'); //直接调用
function test() {
var eval = window.eval,
x = window.eval;
eval('hello'); //直接调用
x('hello'); //非直接调用
}
看到规律了吧?只要是祼的eval
调用就是direct call,前面不要有宿主对象,且函数名一定要是eval
字符串。
再回到闭包
了解了什么是直接eval调用后,如果我弄出这样的代码呢?
function getClosure() {
var a = 1;
var b = 2;
var eval = function(x){
return x;
}
return function inner(val) {
debugger;
console.log(eval(val));
}
}
var func = getClosure();
func('a');
func('b');
在getClosure
中给一eval赋成一个完全不相干的函数,进入到debugger时,能不能访问到a
和b
这两个变量呢?思考一下。
最后
从eval可以管窥出来,ES5的规范几乎是从引擎现实者的角度来考虑问题。如果上下文中没有eval的话,那么闭包中的变量将被很好的释放掉(并不完全正确,参见A surprising JavaScript memory leak found at Meteor),因为引擎可以检测出哪些变量会被引用到,而哪些不会。
对于ES3的引擎而言,如IE6,IE7, IE8,无论闭包中是否包含eval,它们都不会释放掉那些再也引用不到的变量。原因在于,ES3规范中没有对非直接eval
调用进行规范,如下代码在IE6,7,8能通过,而在现代浏览器中会报错:
function getClosure(){
var a = 1;
var b = 2;
return function (func, val) {
alert(func(val));
}
}
var f = getClosure();
f(eval, 'a');
f(eval, 'b');
结论
- 不要在闭包中用eval,许多垃圾将得不到回收。
- 不要采用eval当函数名,引擎会误认为那是一个direct call to eval的,然后内存得不到释放。
- 从理论上而言,ES5比ES3要有更好的性能,ES5更多地考虑到了内存管理的问题。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。