Wednesday, March 4, 2009

Installing top-level code with JavaScript's eval

JavaScript is wonderfully dynamic, so it is odd that its eval function is so unportable.  I already knew that it was tricky if not impossible to use eval to install code in an arbitrary nested scope.  Today I learned that even the simple case of installing code into the global scope is different on each browser. Here's what I found after some digging around on the web and some experimentation.

First, there are a lot of web pages discussing this topic.  Here's one of the first ones I read, that tipped me off that there is a well-known problem:

http://piecesofrakesh.blogspot.com/2008/10/understanding-eval-scope-spoiler-its.html

The following page also discusses the problem, but has a really good collection of comments:
UPDATE: Prototype has gone through the same issue, and come up with similar conclusions as mine.  Here is a page with all the bike shedding:



Based on reading these and on tinkering on different web browsers, here are some techniques that look interesting:
  1. window.eval, what I tried to begin with
  2. window.eval, but with a with() clause around it.  Some people report better luck this way.
  3. window.execScript, a variant of window.eval
  4. window.setTimeout
  5. adding a script tag to the document

What I did in each case was try to use the technique to define a function foo() at the global scope, and then try to call it.  I tested these browsers, which I happen to have handy:
  1. Safari/Mac 3.1.1
  2. Firefox/Mac 3.0.6
  3. Firefox/Linux 2.0.0.20
  4. Firefox/Windows 3.0.3
  5. IE 6.0.2900.xpsp_sp3_gdr.080814-1236 updated to SP3
  6. Chrome 1.0.154.48

Here are the browsers where each technique works.  I lump together the Firefoxes because they turn out to behave the same on all platforms:
  1. window.eval: FF
  2. window.eval with with: FF
  3. window.execScript: IE, Chrome
  4. window.setTimeout: Chrome, FF, Safari
  5. script tag: IE, Chrome, FF, Safari

Conclusions

  1. The window.execScript function is available on IE and Chrome, and when present it does the right thing.
  2. The window.eval function only works as desired on Firefox.
  3. Adding a with(window) around the window.eval does make a difference, but I couldn't get it to do precisely what is needed for GWT.  In particular, GWT does not have a bunch of "var func1,func2, func3" declarations up front, but such vars are assumed in some of the other web pages I read.
  4. I could not find a synchronous solution for Safari.  Instead, setTimeout and script tags work, but they won't load the code until a few milliseconds have gone by.
  5. Script tags work on all browsers.
  6. Surprisingly, I couldn't get setTimeout to work on IE.  From some web browsing, it looks like the setTimeout callback might run in the wrong scope, but I didn't investigate far.  On IE, execScript is a better solution for the present problem.
Based on these, the following chunk of code is one portable way to install code on any of the major browsers.  It uses execScript if it's available, and otherwise it adds a script tag.
if (window.execScript) {
  window.execScript(script)
} else {
  var tag = document.createElement("script")
  tag.type = "text/javascript"
  tag.text = script
  document.getElementsByTagName("head").item(0).appendChild(tag)
}

The Code
Here is the code for the above examples, for anyone who wants to know the details and/or to try it for themselves.

The wrapper script is as follows:
function installFoo() {
  var script = "function foo() { alert('hi') }"
  // varying part
}
installFoo()
window.foo()

For the versions that install the code asynchronously (setTimeout or script tags), I changed the window.foo() line to be:
window.setTimeout(function() { window.foo() }, 100)


The "varying part" is as follows for each way to load the code.  Note that some of them include a gratuitous reassignment of window to $w; that's how I first ran the test and I don't want to go back and redo all of those.

// window.eval
window.eval(script)

// window.execScript
window.execScript(script)

// window.eval with a with
var $w = window
with($w) { $w.eval(script) }

// setTimeout
window.setTimeout(script, 0)

// script tag
var tag = document.createElement("script")
tag.type = "text/javascript"
tag.text = script
document.getElementsByTagName("head").item(0).appendChild(tag)

1 comment:

Unknown said...

Nice post. Nice solution.