Today, I ran some tests to help me understand the scope in which an eval runs. Turns out, like so many things in the browser world, it's very unpredictable and exhibit different behaviors in different browsers.
Let's start with the following snippet of code. I've added comments to demarcate areas in the code, which I will be changing with each iteration.
var foo = 123;
var bar = {
changeFoo: function() {
// We'll keep changing the following snippet
alert(this);
eval("var foo = 456");
// Changing snippet ends
}
};
bar.changeFoo();
alert(foo);
A little explanation of the code above. foo is a variable in the global scope, and it's value is set to 123. An object bar is created with a single method changeFoo which does an eval. The eval creates a local variable (thanks to the var) foo, and sets it's value to 456. bar.changeFoo is called, and the value of the global foo is alerted.
The aim is to test the scope in which eval runs. If eval is in the global scope, the global variable foo should change it's value. If eval is in the local scope, the global foo should be unaffected. Then there are various things we can do inside the changeFoo method which should keep altering the scope of this, so we are also alerting this to see what happens.
The findings are listed below:
| Changed snippet | Internet Explorer | Safari 3.x | Firefox | Google Chrome | Safari Nightlies | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| foo | this | foo | this | foo | this | foo | this | foo | this | ||
| 1 | | 123 | object | 123 | object | 123 | object | 123 | object | 123 | object |
| 2 | | 123 | object | 123 | object | 456 | object | 123 | object | 456 | object |
| 3 | | error | object | error | object | error | object | error | object | error | object |
| 4 | | 123 | object | 123 | object | 456 | object | 123 | object | 123 | object |
| 5 | | 123 | object | 123 | window | 123 | window | 123 | object | 123 | window |
| 6 | | 123 | object | 123 | window | 456 | window | 123 | object | 456 | window |
| 7 | | 456 | object | 456 | object | 456 | object | 456 | object | 456 | object |
| 8 | | 456 | object | 456 | object | 456 | object | 456 | object | 456 | object |
What I think of these results:
- 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.evalis different fromeval, case 3 shouldn't all have given errors. Someone please help me understand that $hit. - Case 4 makes sense, but that's a non-standard behavior in Firefox. Understandable that no one else exhibits it.
- IE baffles me in case 5, and Chrome seems to ape it. In this scenario, the anonymous function is supposed to have the global scope - so, in this case,
thisshould point to the window. WTF is happening here! - Consistent with case 2 above, Firefox and Safari Nightlies continue to display weird behavior in case 6. For some reason, in these two cases, the
evaloperates in the global scope. - Now, I have no idea why, but only cases 8 and 9 seem to really work at all. This is despite Doug Crockford going on and on about not using
withconstructs. It's also despite being beyond (my) understanding about why thewithshould make any difference to theeval, sinceevalis part of the window object.
All in all, if you are going to be evaling JavaScript (not JSON), and you want the eval'd code to run in the global scope, you should use the with block around the JavaScript snippet. Or else, you can lose a lot of hair handling cross-browser issues.
Hope you don't lose as much hair as me.

13 comments:
Thanks for sharing your findings. However for me in Firefox 3 variation 7 and 8 only work as expected if foo exists in global scope at the time the string is eval'd. Otherwise it will just put foo into local scope.
Can anybody confirm this behavior?
IE is handling case 5 more like I'd think. I couldn't find the docs on anonymous function scope and ownership so I can't say if its correct, but I'd think it would be part of its containing scope, like a method.
Interesting... I was looking for a way to run eval in global scope from within objects, but didn't think of using 'with'. Nice to know it's possible after all.
As for case 2... it seems that in FF's case, window.eval isn't the same as eval. (window.eval !== eval)
And eval isn't a standard method on objects (anymore?), which explains 3 vs 2.
Another case you could've tested is eval.call or eval.apply
they turn out the same as window.eval though.
And as an oddity, FF doesn't allow eval.call(this, ...) (same with apply).
(I don't have Safari nightly though, so can't account for any of this there)
I use
with (this) {
// some code
this.doSomething();
}
it seems to work - usually anyway
Firefox 3 and Safari nightlies both follow the ES4 semantics for eval, that is "eval" on its own acts in the scope it is used within, whereas globalObject.eval acts in the global scope.
Neither allows aliases to eval anymore.
anonymous1: You might be right. Will get back to you.
xero: A bug in JavaScript ensures that private functions run on the global scope, even if they have access to the constructor's variables through a closure. It's not what you'd expect, but that the way the language is.
rael: I don't think eval was ever supposed to be a standard method on objects. In fact, now that I think of it, case 3 proves nothing really.
anonymous2: From everything I know, you are not achieving anything by enclosing your code in the with statement.
oliver: You indeed seem right, going by my observations. It doesn't help in today's world, but maybe it's the right thing for the future.
If foo isn't declared in the global scope all your tests will (should) only set it in the local scope. Thus the final alert(foo) will throw.
If you want to execute some script in the global scope you should use dynamic script insertion.
If you want to evaluate some code outside of the local scope - so that local variables don't bleed in - then call eval indirectly via another function. e.g.
function evalScript() {
return eval(arguments[0]);
}
Good one. :)
i think that a timeout runs in global scope, and wrapping its contents in a "quotes" means that it will run the code as an eval.
obviously this has its downsides that it will run at the end of your current running block of code, not at the exact time that it is set (eve if a time of 0 is specified) but it will defiantly in all browsers run in the global scope (and may fix any problem you were looking to solve)
James
There is also a difference between
eval("var foo = 456");
and
eval("foo = 456");
Found a related Ajaxian post from a year ago: http://ajaxian.com/archives/evaling-with-ies-windowexecscript
This is the best way to solve the eval problem:
if (window.execScript) window.execScript(sCode); //ie
else top.eval(sCode); //others
Jery
For a more generalized solution with tests take a look at:
http://caih.org/open-source-software/loading-javascript-execscript-and-testing/
Post a Comment