Perfection Kills

by kangax

Exploring Javascript by example

← back 4073 words

Global eval. What are the options?

David Flanagan recently wrote about global eval() in Javascript, proposing a simple one-liner like this:


  var geval = this.execScript || eval;

Even though it looked beautifully simple, it wasn’t the best cross-browser solution I could think of. Moreover, it’s not something I would recommend. Pondering a bit more on this subject, I realized that there are actually quite a few ways to evaluate code in global scope in Javascript. Some of those ways — e.g. indirect eval — are also not understood very well, and their implications are not immediately visible. David pointed out to me — in one of the comments — that “indirect eval” is not a well-known idiom except in close circles of implementors. And he was right. There was in fact tons of confusion on the subject of global eval, all over the web.

In hope of making the situation clearer, I’d like to go over “global eval” options, taking a look at how and why they work. I’ll also explain some of the problems with the above-mentioned one-line solution.

How eval works

Before going further, let’s define “global eval” as a way to evaluate code in global scope. Simple as that.

The reason for all this fuss about evaluating code in global scope is because global, built-in eval function evaluates code in the scope of a caller:


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

This behavior is described very clearly in both — ES3 and ES5:

[...] The scope chain is initialised to contain the same objects, in the same order, as the calling context's scope chain. This includes objects added to the calling context's scope chain by with statements and catch clauses. [...]

10.2.2 Eval Code [ES3]

In ES5, things are a bit more complicated interesting. The behavior of eval depends on 2 things — whether it is a direct or indirect call and whether invocation happens in strict mode. Don’t worry if you don’t know what “indirect call” is — we’ll take a look at it very soon. When talking about non-strict, direct eval call (i.e. good old eval(...) invocation) the outcome is exactly the same as in ES3 — code is executed in the scope of a caller.

[...] b. Set the LexicalEnvironment to the same value as the LexicalEnvironment of the calling execution context. [...]

10.4.2 Entering Eval Code [ES5]

Eval’ing in global scope

So native eval doesn’t allow to execute code globally. What to do? Let’s first take a look at some of the options, then try to distill them into a cross-browser solution.

Indirect eval call. Theory.

I mentioned “indirect eval call” before, when explaining behavior of eval in ES5. The reason indirect eval is of interest is because in ES5 it actually does execute code globally. Bingo! But what does it even mean — “indirect eval call”. Indirect eval call is simply any eval call that’s not a direct eval call. And direct eval call is defined as:

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]

That might sound a bit cryptic, but it’s actually very simple. What specification is trying to tell is that eval('1+1') is a direct call, but (1,eval)('1+1') is not. If we were to dissect first expression — eval('1+1') — syntactically, we would see that it’s really just a CallExpression, which consists of a MemberExpression (eval) followed by Arguments ('(1+1)'), and in which MemberExpression consists of “eval” Identifier:


    eval             ( '1+1' )
  |______|
  Identifier

  |______|          |________|
  MemberExpression  Arguments

  |__________________________|
  CallExpression

This is, more or less, a signature of a direct call. ES3 has a slightly simpler definition, and the one that’s pretty much identical to the graphic above:

“[…] any way other than a direct call (that is, other than by the explicit use of its name as an Identifier which is the MemberExpression in a CallExpression) […]”

15.1.2.1 eval(x) [ES3]

So eval('1+1') is a direct eval call, but (1,eval)(‘1+1’) is not. Since the latter one is not a direct call, it is therefore an indirect eval call. Let’s take a look at it in more detail:


  (     1        ,         eval  )        ( '1+1' )
     |____|   |_____|    |_____|
     Literal  Operator   Identifier

     |_________________________|
     Expression

  |______________________________|
  PrimaryExpression

  |______________________________|        |________|
  MemberExpression                        Arguments

  |________________________________________________|
  CallExpression

In the above example, the part before Arguments (invocation parens) clearly doesn’t consist of just “eval” identifier. There’s a whole other expression, consisting of comma operator, numeric literal, and only then “eval” identifier. 1,eval — based on how comma operator works — still evaluates to a standard, built-in eval function, but overall expression is no longer a direct call. It is therefore an indirect eval call.

Indirect eval call examples

If you’re still not quite able to recognize indirect eval calls, these are some of the examples:


(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')('...')

According to ES5, all of these are indirect calls and should execute code in global scope. I’d love to go over each one of them in more detail, but that would be too much for this post.

Note the var e = eval; e('...') on 5th line.

You can see that this is essentially what David’s solution does — var geval = window.execScript || eval. When geval is executed, geval identifier resolves to a global, built-in eval function, but overall expression does not have “eval” as an Identifier in CallExpression. Instead, there’s “geval” identifier, making this an indirect call and evaluating code globally.

But we’ll get back to that one-liner later, and now let’s take a look at one last, but not least important point about indirect eval call.

Have you noticed how ES5 definition says that eval from CallExpression should evaluate to standard, built-in function? It means that taken out of context, eval('1+1') is not necessarily a direct call. Only when eval actually references standard, built-in function (wasn’t overwritten or shadowed), a call can be considered direct.


  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

Now that we looked at a list of indirect calls, how about few examples of direct ones?


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

Looks pretty straightforward, doesn’t it?

But wait a second. Why is it that (eval)('...') and (((eval)))('...') are considered direct calls? Certainly, they don’t adhere to the signature we established earlier — Identifier “eval” within MemberExpression within CallExpression. What’s going on here? Shouldn’t the parenthesis around eval make it an indirect call?

The answer to this tricky question is in the first paragraph of ES5 direct call definition — the fact that “eval” in CallExpression should be a Reference, not a value. During program execution, eval in eval('1+1') expression is nothing but a Reference, and needs to be evaluated to a value. Once it’s evaluated, the value is (most likely) a standard, built-in function object. What happens in previously dissected (1,eval)('1+1') indirect call, is that (1,eval) expression evaluates to a value, not a Reference. Since it doesn’t evaluate to a Reference, it can not be considered a direct eval call.

But what about (eval)('1+1')?

The reason (eval) is considered a direct eval call is because (eval) expression still evaluates to a Reference, not a value. Ditto for ((eval)), (((eval))), and so on. This happens because Grouping Operator — “(” and “)” — does not evaluate its expression. If a Reference is passed to a Grouping operator — “(” and “)” — it still evaluates to a Reference, not a value.


  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

Speaking in terms of ECMAScript, this is because both — comma operator (in (1, eval) example) and = operator (in (eval = eval) example) perform GetValue on its operands. As a result, (1, eval) and (eval = eval) evaluate to a value, whereas eval and (eval) — to a Reference.

Hopefully, it is now clear why (eval)('...') and (function(eval){ return eval('...') })(eval) are direct eval calls, but (1, eval)('...') and this.eval('...') aren’t.

Indirect eval call. In practice.

With all the theory of indirect eval behind us, let’s consider practical implications. We already know that ES5 specifies such calls to evaluate code in global scope. There are only 2 things left to consider — ES3 and reality. The fun part starts with ES3, which allows indirect eval calls to throw an error. Not only does ES3 allow for an error, it says nothing about evaluating code in global scope. And what about reality?

It turns out that quite a lot of environments nowadays more or less follow ES5 behavior, evaluating code in global scope. But unfortunately, not all of them.

IE<=8 executes code in the scope of a caller, as if indirect call was direct one. Some versions of Konqueror (~4.3) and Safari <=3.2 do the same thing as IE, evaluating code in the scope of a caller. Older Opera (~9.27) goes even further and actually does throw error, as permitted by a spec. All of this makes indirect eval call (on its own) not a good candidate for a cross-browser “global eval” solution. Not for another few years (when Opera <=9.27, Safari <=3.2 (which is probably still present in earlier iPhones), and other “misbehaving” browsers “disappear”).

Curiously, as of now, even the newest browsers deviate from ES5. Chrome 9, for example, considers this an indirect call — (function(eval){ return eval('x'); })(eval) — which it really isn’t; eval is a reference, it’s part of a MemberExpression within a CallExpression, and evaluates to a standard, built-in function.

Opera 11 and Firefox 4 — both browsers are still in beta for these versions — consider the following expression to be a direct call, even though it doesn’t satisfy all requirements of such (eval doesn’t resolve to a standard, built-in function in this case):


  eval = (function(eval) {
    return function(s) {
      return eval(s)
    };
  })(eval);

  eval('...');

Full ES5 conformance is clearly yet to come (meanwhile, take a look at my simple test suite).

window.execScript

Fortunately for us, there’s a proprietary window.execScript function present in IE. It’s defined to “execute the specified script in the provided language” and allows to execute Javascript code in global scope. It’s not very surprising that Chrome has this function as well; most likely for compatibility reasons. As of today, window.execScript is still present in Chrome 9.

The way window.execScript differs from indirect eval call is that it doesn’t return a value. Actually, it does return value in Chrome, but that value is always null.


  window.execScript('1+1'); // null in Chrome, undefined in IE
  (1,eval)('1+1'); // 2

window.eval

Another, very popular way to execute code in global scope is using window.eval. What’s interesting about it, is that window.eval('...') is often believed to be somehow special when it comes to executing code globally. It’s as if eval called as a property of window object always executes in global scope. The truth is that window.eval('...') is nothing but a… indirect eval call, and is not much different than (1,eval)(‘…’), this.eval('...'), or (eval = eval)('...') when it comes to evaluation.

Not understanding what’s going on, some people unfortunately come up with rather monstrous solutions like eval.call(window, '...'), window.eval.call(window, '...') and even scarier combinations. All of those are, once again, nothing but indirect eval calls. The reason code is executed in global scope is not because eval is called in context of window, but because it’s invoked as an indirect call. eval might as well be a property of foo object, and would still work identically:


  var foo = {
    eval: eval
  };

  foo.eval('...'); // behaviorally identical to `window.eval('...')`
                   // both are indirect calls and so evaluate code in global scope

More confusion can be seen on StackOverflow, where eval is labeled “magic”, and is claimed to somehow lose its magic when aliased — all the symptoms of direct vs. indirect calls.

eval context in webkit

It’s worth mentioning that in some of the WebKit -based browsers — including latest Safari (5) and Chrome (9) — eval throws error when called from within certain context (a.k.a. “this” object). And by “certain context” I mean anything that’s not a window/global object (or at least it seems that way). The error is EvalError: The “this” object passed to eval must be the global object from which eval originated. This is probably done for security reasons.


  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)

Since all of those calls are indirect ones, such error is allowed by ES3 (see, for example, EvalError — 15.11.6.1)

new Function

Sometimes I see suggestions to use new Function (or just Function) to evaluate code globally:


  Function('return 1+1')(); // 2

This is somewhat misleading.

The code executed from within function created by Function constructor doesn’t really execute in global scope. However, it doesn’t execute in local scope either, which is what probably leads to confusion. Function constructor creates a function whose scope chain consists of nothing but a global scope (preceded with function’s own Activation Object, of course). Any code contained in a function created via Function constructor evaluates in a scope of that function, not in a global scope. However, it’s almost as if code executes globally, since global object is the very next object in the scope chain.

What are the implications of this difference?

Well, for once, any function/variable declarations are performed using created function’s Activation Object as Variable Object. In other words, they are declared local to created function, rather than in a global scope:


  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)

Another interesting peculiarity of evaluating code via Function('...')() is identifiers leakage. An example of this is "arguments" identifier which resolves to an arguments object, when evaluating code via Function('...')():


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

Function -executed code is a good solution if you wish to avoid executing code locally, but it won’t execute it globally either.

setTimeout

Non-standard (but finally codified in currently draft HTML5) setTimeout is another way to execute code globally — when given code as a string, rather than as a function. The problem with setTimeout, of course, is that it’s asynchronous and that — similarly to window.execScript — it doesn’t return the result of evaluation.

Unfortunately, looking through HTML5 specs, I couldn’t find any mention of the fact that setTimeout should evaluate code in global scope. Hopefully, I missed it; it should certainly be specified.

Script insertion

Finally, in browser environment, there’s always an option of injecting a script element into a document. Contents of that script element are then parsed and executed as a Program in global scope:


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

As with window.execScript and setTimeout -based solutions, this one doesn’t allow to get meaningful return value (unless some kind of workaround is applied). Script insertion is used by current jQuery (1.4.4) in jQuery.globalEval. It fails to provide a return value, but works in more cross-browser manner than “indirect eval + window.execScript” combination. For example, it works in Safari <=3.2 and in older Opera, in which indirect eval calls fail.

The problem with window.execScript || eval

Let’s get back to this very appealing one-liner, taking a look at some of its problems. Although it seems great in theory, its 2 main downsides can be summarized as:

  • Indirect eval is not sufficiently cross-browser; lack of feature-testing
  • Non-standard execScript is preferred to standard eval

As you’ve seen before, indirect eval is still quirky across some of the existing browsers. There are also environments where it hasn’t even been tested in — mobile browsers, non-standard platforms, older, rare or unknown browsers, etc. Relying on the fact that indirect eval executes code globally — just because that’s what happens in some of the newer, popular browsers — is rather careless. A much saner strategy is to actually feature test indirect eval call behavior before attempting to use it.

Another problem of one-liner is that window.execScript is tested for existence first, and only when it doesn’t exist, eval is chosen as a fallback. One of the rules of interoperability is that standard features should be preferred over non-standard ones. For example, since window.execScript exists in Chrome, one-liner will end up “using” non-standard (and less-capable) window.execScript rather than standard eval.

Finally, the assumption that if window.execScript doesn’t exist, then indirect eval must be executing code globally (which is what one-liner does) strikes me as rather unsafe.

There are more similarly non-optimal soutions online, where folks seem to be taking trial-and-error approach, not realizing why things work the way they do. Rakesh Pai is bedazzled by the unexplainable difference between eval and window.eval invocations:

I don’t know what Firefox is doing in case 2, and for some reason Safari Nightlies seem to be following it. Maybe it’s just beyond my understanding, but case 2 is not supposed to be different from case 1. Why does case 2 operate in global scope? If window.eval is different from eval, case 3 shouldn’t all have given errors. Someone please help me understand that $hit.

The fact that browsers do not fully conform to ES5 makes things even worse. For example, with(window) eval('...') is considered an indirect call in Chrome 9, even though as per ES5 it is very much a direct one — the “eval” Identifier is within MemberExpression of CallExpression; it’s a Reference, and it resolves to a standard, built-in eval function.

Feature testing -based approach

Creating a more robust version of global eval is not hard.

The idea is similar — use indirect eval, with a fall back on window.execScript. The important difference, however, is that indirect eval is feature-tested and is only used when it’s safe to do so. One potential problem here is that window.execScript is not tested, but is instead assumed to execute code globally. If you find this assumption unsafe, it should be trivial to add feature-test for window.execScript as well.


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
})();

The benefit of this kind of implementation is that it enables graceful degradation. You can easily find out if globalEval is available in a current environment (since globalEval would be undefined when none of the 2 solutions are available):


  if (globalEval) {
    /* code can be executed globally */
  }
  else {
    /* use fallback scenario */
  }

Note that — as with any indirect eval / window.execScript approach — globalEval will differ in its ability to return a value, depending on which method was chosen.

Global eval in libraries

So what do some of the popular Javascript libraries do? Which global eval solution do they chose?

As we’ve seen earlier, jQuery sacrifices return value for a more extensive browser support, using script insertion technique in its jQuery.globalEval method.

Prototype.js doesn’t even try to evaluate scripts globally — it uses a good old, direct eval invocation. Instead, Prototype.js warns about ramification of local execution in the documentation.

Dojo choses the infamous window.eval variation, but does something really messed up — it uses indirect eval if eval exists on a global object (which should be true in all ES3-compliant browsers), and falls back on regular eval invocation. This kind of fall back obviously results in code executed locally, which should also happen silently. Dojo’s eval behavior will vary wildly depending on the environment, and has a possibility of uncaught error. On a related, humorous note, I was just as stunned by comments around dojo.eval as David Mark in this message.

Mootools uses window.execScript when present, falling back on script insertion (similar to jQuery). jQuery’s script insertion, however, is more robust than Mootools, as they actually test if it results in evaluation of code.

Fuse.js choses a very elaborate and careful strategy. It tests if indirect eval works as expected (catching an error if any), uses it if it does; if indirect eval is defunct, Fuse falls back on script injection. Unlike jQuery or Mootools, it actually tries to return a value of a code executed via script injection. It does so by wrapping contents of code with eval. An interesting side effect of executing code globally via eval is that function and variable declarations create deletable bindings (which is most likely an insignificant detail).

Summary

So what can we learn from all this?

Hopefully, you now have a true understanding of why eval sometimes executes code in global scope. That window.eval(...) is not very special and is just another form of indirect eval. That behavior of indirect eval is specified differently in ES3 and ES5. That behavior of indirect eval in actual implementations is still often sporadic — partly due to transitioning from ES3 to ES5, partly because of uncaught bugs. You’ve seen that blindly relying on indirect eval is unsafe. That implementations of “global eval” relying on indirect eval should employ feature testing, as with so many other things in cross-browser scripting. Finally, that alternative techniques exist and are used by libraries, such as script injection.

Did you like this? Donations are welcome

comments powered by Disqus