David Flanagan最近写了一个关于全局eval的简单表达式,可以用一行式子表示:

var geval = this.execScript || eval;

尽管看起来很简短,但是跨浏览器的兼容性并不好。仔细考虑了下这个话题,我觉得还有一些方法来实现代码的全局执行。而且有些方法--间接eval--并不为人所熟知,而且它们的内涵也不容易让人们所接受,本文主要介绍下该技术。

为了可以更清晰的讲解间接eval,我打算先回顾”全局eval“的方法,并回顾它们是如果起作用的,我也会提到刚刚的单行实现全局eval的缺点。

eval是如何工作的

我们先定义一个概念,“全局eval”也就是将代码放到全局上下文来执行。

我们之所以将”全局eval“这个概念弄得那么复杂,主要还是由于全局内建的eval函数,是在调用eval函数的作用域下执行函数代码。

var x = 'outer';
(function() {
var x = 'inner';
eval('x'); // "inner"
})();

上述例子的结果就是”inner“,eval的代码是在调用eval的上下文中执行。这个行为在ECMAScript3和ECMAScript5中是一样的。

而在EC5中还有一些有趣的事情,eval的行为还依赖两个两件事--其一是否是直接调用,其二调用是否在严格模式下。直接调用和间接调用下文会讨论,而关于在非严格模式并且直接调用eval,与上文提到的行为一样的,代码在调用上下文中执行。

全局上下文下eval

内建的eval并不会在全局上下文中执行代码,我们来看看其他的一些选项,来实现跨浏览器的全局代码执行。

间接eval调用理论

        在EC5中的eval执行时提到了间接eval执行。之所以我们提到间接eval,是因为在EC5中间接eval调用可以使代码在全局上下文中执行。我们先看看直接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 builtin function defined in 15.1.2.1.

15.1.2.1.1 Direct Call to Eval [ES5]

(其实,直接eval调用与引用类型Reference有关,首先调用括号的左边必须为引用类型,而且还有一些条件,即引用类型的base必须为环境上下文对象(AO,VO,Global),而且propertyName必须为“eval”,其他字符串不可以。)

看起来不容易理解,其实按规范来说,eval(“1+1”)就是直接eval调用,(1,eval)(”1+1“)就是间接eval调用。如果我们分隔第一个表达式—eval(“1+1”)--这就是一个调用表达式,由成员表达式(eval)和参数((”1+1”))构成,并且成员表达式由标识符eval组成。

eval             ( '1+1' )
|______|
Identifier |______| |________|
MemberExpression Arguments |__________________________|
CallExpression

这就是直接eval调用方式,在调用括号的左边是标识符,而标识符在操作过程中会创建一个引用类型(Reference),该结构包含base和propertyName属性,在这里,base的值为当前作用域的活动对象AO,propertyName为eval。

关于(1,eval)(“1+1”),我们也可以同样的形式分析:

(     1        ,         eval  )        ( '1+1' )
|____| |_____| |_____|
Literal Operator Identifier |_________________________|
Expression |______________________________|
PrimaryExpression |______________________________| |________|
MemberExpression Arguments |________________________________________________|
CallExpression

在调用括号左边并不仅仅是eval标识符,它是一个完整的表达式,包括组操作符,数字字面量,eval标识符。虽然这样调用eval也能执行,但是这是间接eval调用,为什么这样是间接eval调用,下文会分析。

间接eval调用的例子

如果你还是不确定哪些是间接eval调用,那么请看下列情况:

(1, eval)('...')
(eval, eval)('...')
(1 ? eval : 0)('...')
(__ = eval)('...')
var e = eval; e('...')
(function(e) { e('...') })(eval)
(function(e) { return e })(eval)('...')
(function() { arguments[0]('...') })(eval)
this.eval('...')
this['eval']('...')
[eval][0]('...')
eval.call(this, '...')
eval('eval')('...')

以上所列出的全是间接eval示例,它们都可全局执行代码。

注意第五行var e = eval; e('...') ;这正是Flanagan所实现--var geval = window.execScript || eval的一部分。当调用geval函数,geval标识符被解析为全局内建函数eval,但是在调用括号左边,标识符并不是eval而是geval,因此这是间接eval调用,在全局上下文中执行。

有没有注意到ES5中定义调用表达式中的eval“应该是全局的内建的函数”?这意味着eval(”1+1”)也不一定是直接调用,看下面一例:

eval = (function(eval) {
return function(expr) {
return eval(expr);
};
})(eval); eval('1+1'); // It looks like a direct call, but really is an indirect one.
// It's because `eval` resolves to a custom function, rather than standard, built-in one

虽然仅仅看eval(“1+1”),应该是直接调用无疑,但是此处eval并不是内建的函数,因此它是间接eval调用。

我们看看直接eval调用有哪些方式:

  eval('...')
(eval)('...')
(((eval)))('...')
(function() { return eval('...') })()
eval('eval("...")')
(function(eval) { return eval('...'); })(eval)
with({ eval: eval }) eval('...')
with(window) eval('...')

对于(eval)(“…”),((eval))(“…”)为什么是直接调用呢?其实,对于组操作符”()”,它并不执行表达式,(eval)返回的仍旧是引用类型,同理((eval))也是返回引用类型,而且这两个引用类型的propertyName都是“eval”,而且eval函数也都是全局的,内建的函数。

而对于上文中提到的间接调用形式(1,eval)(“…”),逗号操作符和复制运算符会执行表达式,导致eval创建的引用类型调用内部方法[[getValue]],返回函数对象而不再是引用类型,因此就不满足规范中提到的直接调用eval的条件,为间接调用。

  eval(); // <-- expression to the left of invocation parens — "eval" — evaluates to a Reference
(eval)(); // <-- expression to the left of invocation parens — "(eval)" — evaluates to a Reference
(((eval)))(); // <-- expression to the left of invocation parens — "(((eval)))" — evaluates to a Reference
(1,eval)(); // <-- expression to the left of invocation parens — "(1, eval)" — evaluates to a value
(eval = eval)(); // <-- expression to the left of invocation parens — "(eval = eval)" — evaluates to a value

间接eval调用练习

        我们已经知道在ES5下,间接eval调用可以将代码放到全局上下文中执行,但是还有2件事情需要考虑--ES3中的情形和实际js引擎实现情况。在ES3中,准许间接eval调用抛出错误。而且ES3中也没有规定代码需在全局上下文中执行。那么在具体的实现中呢?

大多数浏览器是按照ES5的规范去实现的,当然也有一些不是。IE<=8下,这两种方式是一样的,都是在调用上下文中执行代码。Safari<=3.2的行为和IE的一样。Older Opera (~9.27)遇到间接eval调用时会抛错,这是ES3规范准许的。

种种行为提醒我们,间接eval调用的兼容性并不理想,不适合作为全局代码执行的一种方式。因此我们要寻找解决方案。

window.execScript

幸运的是在IE下有一个window.execScript()函数(IE10中没有)。它可以将代码放到全局上下文中执行,但是该函数并不会有返回值。

window.eval

        另一个的全局执行代码的方式是window.eval.看起来eval作为window的属性,因此代码在全局执行,其实并不是那样的。window.eval仅仅是作为间接eval调用的一种形式而已,和(1,eval)(“…”),(eval=eval)(“…”)差不太多。

var foo = {
eval: eval
}; foo.eval('...',this); // behaviorally identical to `window.eval('...')`
// both are indirect calls and so evaluate code in global scope
                   // return window

上述的调用方式和window.eval是一样的,因此不要误解window.eval()这种形式。

webkit中的eval上下文

值得一提的是webkit系列中的一些浏览器的实现—Safari 5和Chrome 9--当设定确切的上下文时(比如this),eval会抛错。确切的上下文,意味着不是window或者全局上下文。抛出的错误是这样的:EvalError: The “this” object passed to eval must be the global object from which eval originated.

window.eval('1+1'); // works
eval.call(window, '1+1'); // works
eval.call(null, '1+1'); // works, since eval is invoked with "this" object referencing global object eval.call({ }, '1+1'); // EvalError (wrong "this" object)
[eval][0]('1+1'); // EvalError (wrong "this" object)
with({ eval: eval }) eval('1+1'); // EvalError (wrong "this" object)

new Function

我们也都知道通过Function构造函数也可将代码放到全局上下文中执行。但其实这是一个误导。用new Function创建的代码并不是真在全局上下文中执行,而是在创建的函数中执行,只不过该函数的作用域链只包括全局上下文(当然函数的AO是在此之前的)而已。这样,代码看起来像是在全局上下文中执行一样,尽管全局上下文是作用域链中仅有的一个对象。

通过new Function创建的变量等保存在函数的AO中,而不是全局上下文中。

function globalEval(expression) {
return Function(expression)();
} var x = 'outer';
(function() {
var x = 'inner';
globalEval('alert(x)'); // alerts "outer"
})(); // but! globalEval('var foo = 1');
typeof foo; // "undefined" (`foo` was declared within function created by `Function`, not in the global scope)

另外,new Function还会造成标识符泄露。它可以将“arguments”标识符解析为对象:

  eval('alert(arguments)'); // ReferenceError
Function('alert(arguments)')(); // alerts representation of an `arguments` object

综上来看,new Function也不能解决代码全局执行的问题。

setTimeout

当给setTimeout传递一个字符串时,会将其放在全局上下文中解析执行。

Script Insertion

这种方法兼容性非常好。jQuery中也是这样实现全局eval的,但是也存在一个缺点,那就是没有返回值。

var el = document.createElement('script');
el.appendChild(document.createTextNode('1+1'));
document.body.appendChild(el)

window.execScript || eval的问题

之前提到了Flanagan的这种方式也有一些问题,现在详细指出。

  • 间接eval调用是否可行,并没有做特性检测
  • 非标准属性execScript在标准属性eval之前

之前提到有些浏览器并不支持间接eval,可能会抛错,也可能没有效果,因此宽泛的使用间接eval实不可取的。

另外,互用性的其中一个规则是“标准特性应该在非标准特性之前”。因此execScript放在eval之前不可取。

最后,如果浏览器都不值这两种方式,方案并没有提供一种降级的方法。在这里,建议使用兼容性最好的 Script Insertion方案作为最后的降级处理。

间接eval的特性检测

对浏览器是否支持间接eval调用其实很简单。

var globalEval = (function() {

  var isIndirectEvalGlobal = (function(original, Object) {
try {
// Does `Object` resolve to a local variable, or to a global, built-in `Object`,
// reference to which we passed as a first argument?
return (1,eval)('Object') === original;
}
catch(err) {
// if indirect eval errors out (as allowed per ES3), then just bail out with `false`
return false;
}
})(Object, 123); if (isIndirectEvalGlobal) { // if indirect eval executes code globally, use it
return function(expression) {
return (1,eval)(expression);
};
}
else if (typeof window.execScript !== 'undefined') { // if `window.execScript exists`, use it
return function(expression) {
return window.execScript(expression);
};
} // otherwise, globalEval is `undefined` since nothing is returned
})();

这里仍然没有做最后的降级处理,需要你自己添加额外的代码。

总结

所以,我们学到了什么?

  • 我们应该知道什么情况下调用eval可以使代码在全局执行;
  • window.eval使代码在全局执行的原理和其他的间接eval调用一样;
  • ES3和ES5对间接eval调用的处理不同;
  • 只依靠间接eval调用时不可靠的;
  • 不要忘记特性检测;
  • 不要忘记最后的降级方案 Script Insertion;

Global eval. What are the options?的更多相关文章

  1. 读Zepto源码之Ajax模块

    Ajax 模块也是经常会用到的模块,Ajax 模块中包含了 jsonp 的现实,和 XMLHttpRequest 的封装. 读 Zepto 源码系列文章已经放到了github上,欢迎star: rea ...

  2. 命名函数、eval创建局部变量

    1.命名函数 var f = function double(){return x *2;} 该语句将函数绑定到变量f,而不是变量double 匿名的函数表达式: var f = function(x ...

  3. 关于一道面试题【字符串 '1 + (5 - 2) * 3',怎么算出结果为10,'eval'除外】

    最近徘徊在找工作和继续留任的纠结之中,在朋友的怂恿下去参加了一次面试,最后一道题目是: 写一个函数,输入一个字符串的运算式,返回计算之后的结果.例如这样的: '1 + (5 - 2) * 3',计算出 ...

  4. JavaScript 为什么不要使用 eval

    本文内容 eval 隐藏的 eval 安全问题 结论 参考资料   eval eval 函数是一个高等级的函数,它与任何对象都无关.其参数,如果是一个字符串表达式,那么该函数计算表达式的值:如果是一个 ...

  5. javascript: jquery.gomap-1.3.3.js

    from:http://www.pittss.lv/jquery/gomap/solutions.php jquery.gomap-1.3.3.js: /** * jQuery goMap * * @ ...

  6. Babel指南——基本环境搭建

    ECMAScript的现状 ECMAScript,本身是一个脚本语言的设计规范,基于此规范,有许多为人熟知的语言,如JavaScript.ActionScript等.而时至几年前,随着Node.js的 ...

  7. 手动编译ts的经过

    动机 以前写ts或者es6,都是用在脚手架搭建的项目中,比如vue和react,当时当我识图写一个ts的demo的,我还要创建一个完整的vue或者react项目?明显不合适,那就要研究一下如何手动搭建 ...

  8. 使用LaTeX和KnitR自动生成报告

    扩展名为.Rnw(Rtex)的文件就是包含了R代码的LaTeX文档.编译的时候,先用Rscript调用Knitr处理,生成.TeX文档,然后用pdfLaTeX/XeLaTeX编译成PDF. 最方便的编 ...

  9. Redis 4.x RCE 复现学习

    攻击场景: 能够访问远程redis的端口(直接访问或者SSRF) 对redis服务器可以访问到的另一台服务器有控制权 实际上就是通过主从特性来 同步传输数据,同时利用模块加载来加载恶意的用来进行命令执 ...

随机推荐

  1. 使用DataList实现数据分页的技术

    今天做网站的时候,用到了分页技术,我把使用方法记录下来,以便日后查阅以及帮助新手朋友们. DataList控件可以按照列表的形式显示数据表中的多行记录,但是被显示的多行记录没有分页功能,使用起来不太方 ...

  2. Weblogic反序列化漏洞补丁更新解决方案

    Weblogic反序列化漏洞的解决方案基于网上给的方案有两种: 第一种方案如下 使用SerialKiller替换进行序列化操作的ObjectInputStream类; 在不影响业务的情况下,临时删除掉 ...

  3. 有Maple T.A.自有试题图so easy

    对于想完全控制试题库的用户而言,Maple T.A.是最好的选择.不论您是要利用现有的题库,还是要创建自己的题库,Maple T.A.都可以为您提供功能强大.操作便捷的工具创建数学内容. 1) Ste ...

  4. 学习笔记: Delphi之线程类TThread

    新的公司接手的第一份工作就是一个多线程计算的小系统.也幸亏最近对线程有了一些学习,这次一接手就起到了作用.但是在实际的开发过程中还是发现了许多的问题,比如挂起与终止的概念都没有弄明白,导致浪费许多的时 ...

  5. Windows安装和使用zookeeper

    之前整理过一篇文章<zookeeper 分布式锁服务>,本文介绍的 Zookeeper 是以 3.4.5 这个稳定版本为基础,最新的版本可以通过官网 http://hadoop.apach ...

  6. 使用VS2013分析DMP文件

    当一个发布的.NET应用程序出现app crash,无法通过日志分析异常原因时,就需要通过分析DMP文件了,传统方式是通过WinDbg来分析DMP文件,但是WinDbg用起来不是很方便,其实VS就是一 ...

  7. [Voice communications] 声道的转换

    本系列文章主要是介绍 Web Audio API 的相关知识,以及 web语音通信 中会遇到的一些问题,阐述可能存在错误,还请多多斧正! 很多粤语剧都提供了两个声道,一个左声道为粤语,一个右声道有国语 ...

  8. Verlet-js JavaScript 物理引擎

    subprotocol最近在Github上开源了verlet-js.地址为https://github.com/subprotocol/verlet-js.verlet-js是一个集成Verlet的物 ...

  9. Google分布式构建软件之二:构建系统如何工作

    分布式软件构建第二部分:构建系统如何工作 注:本文英文原文在google开发者工具组的博客上[需要FQ],以下是我的翻译,欢迎转载,但请尊重作者版权,注名原文地址. 上篇文章中提到了在Google,所 ...

  10. Redis 主从配置和参数详解

    安装redis 下载redis wget http://download.redis.io/releases/redis-3.0.7.tar.gz 解压redis tar -xvf redis-.ta ...