凑凑热闹,给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带来的生产力.@老赵不是也早就想到了么? 那就是预编译... 这个所带来的额外的成本,仅仅是个习惯问题.你把该做的都自动化以后. 一切都不是问题啦.