Loading…

Background:

近来,随着以Angular为首的诸如Ember.js、knockout.js等框架的大热,WEB的前端开发开始进入了MVVM框架大行其道的时代。老实说,前端技术的迭代实在太快,哪怕是作为一个刚投入浪潮的新生力量我都感觉有些趟不牢,无奈之下开始学习以"关注数据而不是关注DOM"为目标的开发模式,但近一年JavaScript的一些长进又让我不仅仅满足于只是学习框架应用,然而恰逢其时,司徒正美发布了他的mini MVVM框架——avalon.js,并且在github上开源,同时他又出版了耗费其无数心血、历时三年的新书《JavaScript框架开发》,在两相搭配之下,想来avalon则是最好切入点和学习资料了。

在源码的伊始,我便发现一个司徒称之为黑魔法的技巧,后来又偶然在贴吧上议起,因此有了题首的这个问题

(0,eval) && eval in JavaScript

以上为零星的吐槽和抱怨,顺便做一个没什么关系的展开....orz,以下言归正传

正文

众所周知,JavaScript是一门和大部分脚本语言类似、没有访问器修饰符的语言(最近的新贵swift也是如此,但丫可是强类型的静态语言....)。因此,除了闭包内维持的变量,大部分的元素几乎全局可见,然而,我不得不说一声JavaScript果然当的起他"遍地是坑"的雅号,全局可见本身不是什么很严重的问题,但代码同时不可控,那就如同骆驼背上的稻草一样岌岌可危,尤其在页面其余程序片段未知的Web Browser Client端更是如此。

通常来说,整个前端的交互代码几乎是围绕着window,document,undefined等原生元素而展开的,但因为元素是可见且不可控,因此无法准确的保证自己的代码建筑并运行在正确的引用或者作用域中。所以,为了使得自己的程序始终能够运行正常,web developer们往往会独辟蹊径,而题首的(0,eval)则是这些技巧中的一份子。

在这里先释出它实际的作用:由于代码中得独特的组成部分,可以强制其执行上下文保持在全局对象中,因此可以通过其来获得某些重要的原生元素引用——比如window对象(但这可能不是最佳的跨浏览器实践)。

代码发生于avalon.js Line:14

var window = this || (0, eval)("this")

上述代码,最夺人眼球的便是eval函数的特殊写法,如果是第一次看到,人们很容易就会以为这是一种炫技的卖弄,认为执行行为上等同于eval,但事实上,不尽如此。

var x='outer';

(function(){
    var x='inner'
    eval('console.log(x)'); //inner
})()

//在无论ES3还是ES5的规范中,这样的调用的行为的结果是非常清晰并且确定的。
//but
var x='outer';

(function(){
    var x='inner'
    (0,eval)('console.log(x)')//outer
})()
//但在ES5中,这种形态的eval在被确定调用时,引擎的执行选择则会有差别。

上文两段代码论证了eval并不恒等(0,eval),而(0,eval)与eval在差别在于:eval只在调用者的执行上下文中执行,并且它的词法作用域是可以被静态确定的;而(0,eval)则可以保证Arguments中的代码无论在何时何地出现但可以始终在顶部全局作用域中运算执行。

因为按照ECMA的定义,eval这个函数有两种调用模式:

  • Direct call 直接调用
  • Indirect call 间接调用

其中(0,eval)就属于间接调用。在上述代码,两个eval表现行为不同的根本差异即在于此。一个直接的eval函数在解释器的AST抽象语法树中是一个左值引用,它在调用时会根据调用时的environment(即函数上下文)来作对应的具体计算。如果eval作为引用出现,那么就可以认为是一个Direct call。

既然谈到表达式的左右值,这里就插一段解释下左值与右值,来自于RednaxelaFX。

假设有如下表达式:
    i=a+b*c

在上述代码中,赋值符号左侧的i是一个标识符,表示一个变量,取的是变量的“左值”(也就是与变量i绑定的存储单元)

右侧的a、b、c虽然也是变量,但取的是它们的右值(也就是与变量绑定的存储单元内的值)。在许多编程语言中,左值与右值在语法上没有区别,它们实质的差异容易被忽视。一般来说左值可以作为右值使用,反之则不一定。例如数字1,它自身有值就是1,可以作为右值使用;但它没有与可赋值的存储单元相绑定,所以无法作为左值使用。

左值不一定只是简单的变量,还可以是数组元素或者结构体的域之类,可能由复杂的表达式所描述。因此左值也是需要计算的。

上面的一系列文字陈述了什么是Direct call,那么又是受到了什么因素的干扰,导致(0,eval)在AST中最终被解释并且执行成了一个Indirect call呢?

按我手边的红宝书——N.C.Z的《JavaScript高级程序设计》第三版,第54页中描述

“逗号操作符还可以用于赋值”

至此,原因就很明朗了,(0,eval)这个表达式中eval因为逗号运算符的干预被隐性的计算成了一个右值而不再是一个引用,从而使其成为了规范中明确定义的一种Indirect call的形态。

同时在ECMA-262 5th Edition中,有声明如下:

If there is no calling context or if the eval code is not being evaluated by a direct call (15.1.2.1.1) to the eval function then——

Initialize the execution context as if it was a global execution context using the eval code as C as described like this below

  • Set the VariableEnvironment to the Global Environment.
  • Set the LexicalEnvironment to the Global Environment.
  • Set the ThisBinding to the global object.

所以,如果引擎严格按照规范所描述的来实现,引擎最终会对eval在作为引用和值时分别有不同的逻辑来构造其对应的执行上下文,从而导致了两者之间最终产生了差异。

文至末尾,其实,除了上文中所指出的(0,eval)这个用法以外,包括但不限于的以下写法都是Indirect call。

  • (1, eval)('...')
  • (eval, eval)('...')
  • (1 ? eval : 0)('...')
  • (__ = eval)('...')
  • var e = eval; e('...')
  • (function(e) { e('...') })(eval)
  • (function(e) { return e })(eval)('...')

Reference

Tags

JavaScript

About the author
comments powered by Disqus