凑凑热闹,给eval做个科普.

 此篇.本来想多写些测试用例. 但是因为 阿灰,已经做了很多测试.所以就做个科普吧.

 

eval是什么.

我个人觉得eval最初的设计,就是一个内置函数.提供一个动态执行代码的接口. 所以ES3上对他的描述就是如此简单. 这里为了描述清楚ES3对 eval code的规范.所以我不得不拿出一大段来解释这些东西.


ES3 :

 

.Eval Code : 
               当控制器进入一个eval code 的执行环境时,前一个(eval函数调用代码所处的)执行环境,作为调用环境(calling context,调用环境),用以决定作用域链.变量对象,及this关键字的值
               如果没有调用环境,则作用域链、变量对象、以及this值的处理同Global Code相同.(这里原文描述很含糊,所谓没有调用环境,即使指全局环境)
               . 其作用域链的初始化工作,就是按相同顺序,使其包含,同调用环境的作用域链对象所包含的相同的那些对象(活动对象、变量对象,等等等等,甚至是with和catch所添加的那些对象).
               . 其变量对象初始化过程,虽然使用的就是调用环境的变量对象.但Eval Code内的标识符对应属性.不具备任何特性.(这就是为啥eval中 声明的变量,可以被delete 运算符删除掉的本质原因)
      
     
    . this的值,与调用环境的this值相同.(此处,与edition 5所指,非直接调用的eval,视为全局调用并无冲突. 即该情况下,其calling context为global context. 则this应指向global.)
 

 

好吧,我必须承认,这一段看起来很绕. 简单来说,ES3中的eval就是下面这样子:

  1. 它是个内置函数.

  2. 它具备把一个字符串的内容,作为ES 代码,动态执行的能力.

  3. 动态执行代码的作用域隶属于 eval函数被执行的位置,所处的那个执行环境相关联作用域.

  4. 动态执行代码的作用域链,也同样就是eval函数被执行位置,所处的那个执行环境的作用域链.

  5. 动态执行代码中 声明的变量, 我们可以 用delete 运算符删除掉.

标准的不足 : ES3 没有明确说明 其内部的 arguments, 以及 arguments.callee 以及 arguments.callee.caller 应该是谁. 导致各个引擎实现,差异巨大. 比如v8的实现,就很奇葩(不是重点,掠过.).

 

  然后,我们再来看看ES5 :

  ES5,很奇葩的,在ES3的基础上,把eval这货,搞的人不像人,鬼不像鬼,既有内置函数特征,又有关键字特征. 为什么这么说呢?因为ES5引入了一个,被称为direct call(直接调用)的概念.  

直接调用的概念,请猛击链接: 深入剖析,什么是eval的直接调用.

 

下面是简单的几个demo :

;(function(){
  var a = 1;
  var fn = eval;

  eval('typeof a'); //number

  (eval)('typeof a');//number

  (1,eval)('typeof a');//undefined

  fn('typeof a');//undefined
}());

  

 

ps : 
  读了前面的内容,可能这里唯一需要说额外明的只有 (eval) 的情况, 这里分组运算符在生成语法树时被消除了. 所以(eval) 也是直接调用. 就如同(obj.fn()) 中this 指向obj 一样. 而 (1,obj.fn) 中this 则为global或undefined(严格模式).

  那么,direct call,这个设计真的合理么? 我个人觉得这是不合理的设计. 好的设计应该是:
eval('code', scope); scope 可以是布尔, 用来指定是否全局,默认为false. 即非全局,同 direct call的效果. 
true则为全局. 怎么也好过这种让人蛋疼的设计. 进一步讲,这种设计, 还可以让在将来,让我们指定eval的scope变为可能. 
当然.这个如果设计的不好,就成了第二个 with了... 也不是现在我们要关心的地方.

 

  ES5 严格模式对eval的影响 :

  ES5,在定义了奇葩的eval后,又搞出了新花样,因为ES5引入了严格模式的概念,所以严格模式对 eval的影响,是我们不得不提到的.

  严格模式下,eval代码中的变量初始化..其外部不再可访问,也就是说eval有了一个独立的变量环境(参考ES3的variable object)

  ES5严格模式科普链接 : http://www.cnblogs.com/_franky/articles/2184461.html

  参考代码:

   'use strict';
    eval('var a = 1;');
    typeof a;// undefined

  

  ps : ES3,和ES5 的差异.导致浏览器不同,浏览器版本不同,他们基于的ECMAScript标准版本不同.导致了各种实现差异.不在本文讨论范围...

  以上部分,就是ECMA262对eval的一些定义和科普部分.. 后面,则会介绍一些有趣的东西.

 


 

我们一直耳熟能详的一句话出自老道之口 :  eval is evil .    请容我在此篇里,唯一想输出的看法就是, 这句话要有前提,那就是使用不当. 我这里仅指出,可能存在的副作用.  如何使用eval,则是大家自己思考的事情. 可能我会在最后面提我自己的一些权衡.

 

demo1 :

<script>   

 // eval('var a;');
 
</script>
<script>

  var a = 1;
  delete a;
  alert(typeof a)
    
</script>

这个例子中,注释掉eval部分和不注释的结果存在差异. 这显然是一种副作用. 而且是后面的脚本块中delete a;操作之前,没办法修正的.

导致这个问题的原因简单解释下: 变量初始化阶段,初始化的变量,会有一些特性(ECMAScript内部用于描述属性状态的东西.),其中有一个特性,ES3称为{DontDelete},ES5称为[[Configurable]],被应用来描述其是否可以被删除.或特性是否可被改写.   而前面我们知道, eval内部声明变量,ES3中,不具备{DontDelete}特性,或ES5中被描述为 其[[Configurable]] = true. 那么就导致该变量可以被delete删除.  又因为,ES中对变量初始化过程,有严谨描述. 即.遇到变量声明, 就去看当前变量对象(ES5,被称为个环境记录)是否有同名属性.如果有,就什么都不做. 所以先声明的变量,具有优先级(函数声明和形参则不同于变量声明,不属于本文讨论范围).  所以结论就是,非严格模式下, eval内的标识符声明.有副作用.

 

 

demo2: 上一组图来说明.(注意,该例子,仅用于说明v8引擎.不同引擎实现会有差异.)

(1).

 

(2).

 

 

显然,这两张图,充分说明eval ,对v8的一些优化策略,是有影响的. 当然,能起到类似影响的,不仅仅是eval ,如果这里我们return一组函数,互相之间内部引用的变量不同.同样会导致类似的问题.即函数a中引用,而b中没有引用.但是都会被扁平化的保存在作用域链的上一层中.  这里我和阿灰有少许分歧,他认为v8为了更好的GC,做了这件事, 我认为是为了减少扁平化处理作用域链,所带来的额外的内存占用. 但是我们都是黑盒推测. 所以仅供参考啦.

 

 

demo3 :

参考代码:

 var fn = function (x) {
        return x + x ;
    }
   
    
    var test = function () {
        return function(x){
           return fn(x);
        }
        
        //eval('')
        
    }();
    
    console.time('test');
    for (var i = 1000000; i--; test(1));
    console.timeEnd('test');
    
    //eval('')

demo3中注释掉的两个不同位置的eval,对v8性能的影响程度会不同. 但内层的eval,影响程度会是没有eval时的20倍.  即使它出现在 return 后面, 而永远不会执行.

此处数据有误,开启console,似乎会影响跑分.把console面板关闭. 直接跑,用alert输出,则虽然eval的出现,仍然有性能损耗,但是其影响,微乎其微. 就上面的例子,有没有eval,实际只有100ms的损耗.  而不是xxx倍.

所以v8一个简单粗暴的做法就是. 词法、语法分析期,发现eval,并且确认它属于direct call直接调用. 就会不同程度的干掉一些优化策略... 这里的优化策略,类似被称为fast property access 的原型链优化方式.  通过对"热代码"所在执行环境,创建隐藏类(hidden class) 来实现.一种内联绑定的概念. 用这种绑定关系,代替原始代码中的标识符或属性查找. 显然,这种优化,在很深的作用域链,或很深的对象属性查找中,在热代码中,对性能有十分可观的提升. 这种机制,类似前面提到的v8引擎,扁平化作用域链的实现机制. 但是很遗憾. eval('')破坏了这一切.原因是eval内部可以在当前作用域中插入额外的东西,包括函数,变量,等.因为eval的参与,会导致,实际的标识符或表达式,进行evaluate的结果发生改变.导致 优化的内联绑定不是正确的结果. 

 


就写到这吧.再不睡觉,明天没法上班了... 回头有时间,再填充些demo,或者补下遗漏的东西吧.....

哦对,表达下我对eval的看法, 该用就用,如果确认没有安全问题, 又没有对系统造成性能瓶颈. 大胆的用吧.. 因为在某些需求上,它是一个很好的选择.... 好吧,这部分,以后有时间再补吧.....挺不住了....

 

 最后,转帖下阿灰的,对于以上问题的更多测试,以及结果,仅供参考:

http://www.otakustay.com/eval-performance-profile/

http://www.otakustay.com/about-closure-and-gc/

 

 

 


 

 补充之前没说到的三件事:

1. eval('xxx') 本身的情况. 

  v8引擎中,对 eval('xxx'), Function('xxx') 的结果,会进行缓存. 多次执行相同内容的话,性能问题并不那么严重. 部分js模板,也采用类似的缓存 编译结果的方式.也是出于类似的考虑.

 

2. Function, 以及 非direct call的 eval ,在demo2中,不会有副作用.因为他们的calling context被视为同 global code中的eval.

 

 

3. 权衡.

  这一点考虑再三还是写一下,我个人的看法. 我认为eval在必要时,是完全可以用的. 比如@老赵的 wind.js的场景.  事实上,在众多国内框架中,老赵的wind.js真是我十分喜爱的一个东西. 创新,特定场景需求.生产力. 都是他的价值. 只有当你被 各种深度嵌套的异步回调,多次强奸的时候.你才会试图去改善开发方式. 也许你试过promise pattern 的各种框架. 但在我看来,从生产力的角度,完全无法和windjs 相媲美.    

 

  另外说说我实际项目中遇到的一个情况. 我们有个特定的序列化,反序列化数据的需求. 但是为了同时对数据有校验性,我们把一些token作用的东西,并入到 序列化反序列化的规则中去. 这时候,我针对性的写了一个,JSON文法的子集约束(并不是真子集,而是子集基础上,额外又增加了一些语法限制). 通过巧妙的规则和约束上的设计.我砍掉了需要语法分析才能处理的情况. 这样就只需要实现一个状态机.只做词法分析,就能完成反序列化工作 以及 token校验.  我甚至为了提升性能, 违背状态机设计的一些原则. 比如,把 undefined 的9个状态.放在一个状态中,以避免状态迁移带来的性能损耗. 所以我的ll1 词法分析器是一个投机取巧,违背设计原则的产物.但是,它实现了反序列化时,时间复杂度O(n) ,所以它带来了一定行的性能.  但是最终这个方案被废弃. 而改用正则校验+eval. 原因是eval更快. .这时候性能问题主要集中在正则上(不在本文讨论范围). 所以eval的慢.要看和谁比.不是么?  而且.一但反序列化的数据有重复出现的情况,还可能在高级浏览器中,命中缓存... 当然,其实在你不需要依赖作用域链的场景. Function 来动态执行,可能是更好的方案. 但是总有些场景你离不开他. 那就用吧.

 

    最后,只有你真正认为eval的使用,导致内存开销和性能损耗 在某些高性能需求方面,无法接受. 但是又舍不得windjs带来的生产力.@老赵不是也早就想到了么? 那就是预编译... 这个所带来的额外的成本,仅仅是个习惯问题.你把该做的都自动化以后. 一切都不是问题啦.   

 

 

 

 

posted @ 2012-08-16 03:44 Franky 阅读(...) 评论(...) 编辑 收藏