深入剖析,什么是eval的直接调用.

本文是对上一篇随笔 凑凑热闹,给eval做个科普. 的扩展阅读.

如果您没看过上一篇,我建议您先去看看,然后再看此篇. thx.

此篇,大量引入ES5 的概念以及名词. 实属无奈,因为直接调用概念来自ES5. 如果你觉得阅读这些东西有些浪费时间. 我也尝试给一个白话文的解释.

ES5设计直接调用的目的就是, 让eval 有改变eval动态执行代码的scope 为global object的这一能力.  但是很不好的是.他们通过直接调用来实现这种,很抽象的概念来实现这个能力. 而后面那些官方咒语般的描述的本质,其实是想说明一层意思 . 就是 , 如果你的语句里 ,eval 是一个看起来独立调用, 不转借它人, 对 eval的使用,好像一个关键字来用的那种感觉.就算作直接调用,那么eval内的动态执行的代码,就如同是执行在eval被调用的函数内,而对应的非直接调用则如果global code一样.(直接体现作用域的影响).但是这种含义,太难描写了. 所以 ES5蛋疼的出现了下面的那种定义... 好吧. 如果你觉得看到这里就解决了你的困惑, 我就十分赞成,你放弃后面部分的阅读...直接离开了. 如果你非想问个为什么...那就继续看吧...

 

 

 

eval 直接调用,原文描述 :

A direct call to the eval function is one that is expressed as a CallExpression that meets the following two conditions:

1. The Reference that is the result of evaluating the MemberExpression in the CallExpression has an environment record as its base value and its referenced name is "eval".

2. 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 的定义章节).

 

解释前的准备:
在描述前要简单介绍下原文中提到的两个词法非终结符.MemberExpression , CallExpression.
CallExpression , 你姑且认为 fn('Franky') 就是一个CallExpression,但其实 fn('Franky') 只是符合CallExpression的一种情况,即 MemberExpression Arguments 这一个语法产生式,你姑且看成fn对应MemberExpression ,('Franky')对应Arguments.


词法、语法相关知识不是本文重点....就此打住.

 

简单来说, ES5定义的 eval的调用 要满足下面俩条件,则被视为 直接调用:


1.解释执行CallExpression中MemberExpression的结果,必须是一个,其base值为一个环境记录(environment record),且 referenced name为 "eval" 的引用类型.

   
2.把步骤1的结果作为参数,进行GetValue抽象运算的结果,必须就是内置方法 eval.

 

 

 

名词解释:

environment record(环境记录):

  有两种环境记录值.被ES5所定义.分别是:声明式环境记录(declarative environment records)对象环境记录(object environment records).声明式记录是用于定义ECMAscript语言的,语法元素中如 FunctionDeclarations,VariableDeclarations,和Catch子句与ECMAScript语言值,直接关联的标识符绑定特性. 对象环境记录则是用于定义ProgramWithStatment这样的.某对象属性上发生的标识符绑定.  要解释清楚这两种东西. 需要更加庞大的篇幅,所以本文就不再做展开的说明了. 你姑且把环境记录,看做ES3中的 变量对象. 它的作用就是维护标识符,比如 变量声明, 函数声明, 函数的形式参数.等. ES5只是把 变量对象更加细化的分为两个类型.分别用于说明不同场景的标识符的维护方式的差异. 

 

 

 

Reference Type(引用类型):

  引用类型,不是语言层面的数据类型. ECMA262中定义它,是为了更好的对规范进行解释说明. 但是ECMAScript的实现.必须在内部操作中,参考此处的描述. 引用类型的值仅仅用来作为表达式运算的中间结果.并不能用来给一个变量或属性持有. 
  引用类型是设计用来解释一些如 delete、typeof、=(赋值)  等运算符的行为的. 即解释执行(evaluate)语句、表达式、以及 GetValue的求值过程.
      举个例子: 赋值运算的行为可以使 "=" 赋值运算符左边的运算元(左值表达式).预期运算结果为一个"引用". (这并不是说 variable = 'Franky'; 这种赋值语句的结果是引用类型,而是指对variable 左值表达式的解释执行的中间过程,产生一个引用类型用于后面的赋值运算.)

ES5 :

  由 base value, referenced name(同edition3的 property name), 以及一个叫做 strict reference 的布尔值(用于严格模式).构成的一个内部对象.这货其实就是描述, 某个对象的 叫做xxx的属性. 对其进行 GetValue 运算,就会调用该对象的内部方法[[Get]] 来获取该属性的值.那么 base有可能是一个对象,一个字符串,一个数字,一个布尔值,或undefined, 还有一个就是我们关心的被ES5 称为 environment record(环境记录,对应ES3的变量对象)的东西

 

  别奇怪为啥base可以是字符串什么的(这是ES5与ES3的一个区别之处)

  参考 -  'Franky'.length  语句按照ES5的的描述,其解释执行过程大概是这样子:

1. 先把'Franky'.length 转换为 'Franky'['length']
2. 解释执行 'Franky'
3. 对 步骤2的结果,进行GetValue运算(对解释执行的结果求值的抽象运算,运算元可以是一个原始值,或一个引用类型,好吧又绕回去Reference Type了)
4. 解释执行'length'
5. 对步骤4的结果进行GetValueu运算
6. 对步骤3的结果进行 ToObject运算(尝试进行装箱操作.参考new String('Franky'))
7. 对步骤5进行ToString运算(因为'length' 这部分可能是一个表达式,所以要先解释执行,然后求值,然后再尝试转为字符串.才可能符合属性访问器的语法)
8. 返回一个 base值 为步骤5的结果(ES3则是步骤6的,这也是为啥ES5 的base,可以是各种奇葩原始值的原因之一..). referenced name 为步骤7结果, strict reference 为代表当前执行环境是否为严格模式的布尔值. 的引用类型

 

  好了,后面如何返回字符串Franky的字符数的,不属于本文讨论范围(相信你见到new String('Franky')的装箱过程,应该想得到...).

  而上面这个解释执行过程,ES中被称为属性访问器( Property Accessors) .这个属性访问环节,总是要返回一个reference type.然后再进行后面的操作.

  列出这个过程,只是想让不熟悉ES的朋友,对ES的 Reference Type,有个直观的感受.

 


GetValue:

你姑且认为他就是对一个引用类型 求值的过程.因为引用类型实际作用是对 某对象上某个属性的一种描述.所以本质是对就是获取该对象某个属性值过程(涉及到对象的内部方法[[Get]],不再详细介绍). 当然内部过程不是如此简单. 比如reference中,base 值为
undefined,123,等等.
而当base值为一个环境记录时,意味着这个引用类型其实是一个被称为 Identifier Reference的东西. 比如 对 var abc = 123; 中 abc 进行解释执行的过程,会得到一个标识符引用.就是这个意思.

 


有了上面这些基础后,我们再回过头来看 上一篇 凑凑热闹,给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
}());

  

 

语句 eval('typeof a') :
  符合两个条件- eval 作为callExpression中的MemberExpression部分,本质上是一个标识符解析,返回一个Identifier Reference(标识符引用). 其base value 即是充当 environment record 的 global object. 而 referenced name显然就"eval".

语句 (eval)('typeof a') :
  符合两个条件- 原因上一篇提到过,( xxx ) 中分组运算符,在生成抽象语法树时,只是影响语法树生成过程,或者说是影响其他元素在语法树中的位置和顺序(从程序运行角度来说,本质是对运算优先级的影响),但不会作为节点保留在语法树中.也就是在运行时,不会 有额外的与其有关的运算.所以之前我会说它在语法分析期.尽到了义务后,就被消除了.(此处罗嗦的解释一下,只因为,昨天有朋友表示之前的简单描述,无法解决他的困扰.). 那么这里的情况其实就和第一个demo是一样的了.


语句 (1,eval)('typeof a')

  不符合条件1中,结果为 Reference Type这一要素. (1,eval) 中分组运算符,虽然同demo中一样,被消掉. 但是 1,eval 这个表达式 实际的含义是 "," 逗号运算, 即 1和 eval 都是逗号运算符的运算元. 其运算结果为对,eval解释执行后获得的reference 进行GetValue的结果. 显然因为多了一次GetValue 导致结果不符合这个条件了.


语句 fn('typeof a')

  不符合条件1 中referenced name为 "eval" ....


下面我要额外说的是一个 特殊的东西.

语句 window.eval('typeof a') //undefined .

为什么要额外补充这个呢,因为其比较特殊. 首先,它 不符合的是 其base值为一个环境记录(environment record) .这一点,对于熟悉ES5的朋友,可能会比较困惑..因为:


1. window,在ES层面等价global (虽然绝大多数浏览器的实现,并非如此)

    参考 :http://www.cnblogs.com/_franky/archive/2010/12/20/1911328.html


2. global object 在某些情况下,是作为environment record 存在的.

 

3. window.eval  , 这种被称为 Property Accessors 的东西.解释执行的结果,就总是一个 base value 为 GetValue(evalute(window)) ,referenced name 为 "eval" 的一个 引用.  而对这个引用进行GetValue运算得到的结果又一定是 内置方法 eval (除非有人劫持了他们). 

 

那么看起来window.eval似乎符合 两个条件啊.. 其实不然. 设计上, environment record 对外是不可见的. 从window访问global时, 显然算做可见的访问. 而不是通过词法环境(Lexical Environments, 好吧又一个新名词.对应ES3 的作用域链的实现机制.前面提到的environment record就是挂在Lexical Environments下面)来找的. 只有发生类似作用域链中查找标识符情况.找到global时它才是environment record. 才会满足条件. 这也是为啥 在任何执行环境(很深的内层函数嵌套中) eval('xxx') 最终找到global.eval.但是却算作直接调用的原因.

注: 应该注意那些,按照ES3标准实现的引擎的行为. 与本篇描述不符. 比如悲剧的IE8- 的jscript引擎.和其他主流浏览器的部分早期版本.

 


好吧.到了此篇该结束的时候了. 最后送上一个小demo .你能不测试就说出结果么?

 

;(function (eval) {
  var a = 'franky';

  eval('alert(typeof a)');

})(eval);

  

 

 

 

posted @ 2012-08-18 04:50 Franky 阅读(...) 评论(...) 编辑 收藏