Tuesday, October 14, 2008

Today's JavaScript kookiness

One of the weirdest aspects of JavaScript's eval is that it not only has complete access to the lexical scope of its caller, but it even gets to add variable declarations to its caller's lexical frame. So I can write
(function() {
eval("var x = 10")
return x
})()
and get 10. Now, bearing in mind that var declares a binding for the entire containing function, eval combines in a truly spectacular way with lexical bindings. What does the following test function produce?
function test() {
var x = 'outer'
return (function() {
{
let x = 'inner'
eval("var x = 'eval'")
}
return x
})()
}
Clearly the let-binding of x seems to be in scope for the eval code. So we might expect the function to return the string 'outer', with the assumption that the eval code simply modified the inner binding. Then again, the eval code is creating a var-binding, so we might expect the function to return the string 'eval', with the assumption that the eval code is declaring a new variable x to be local to the inner function. So what does it really return? undefined.

What?

If I've got this right, what's happening is that the eval code is creating a var-binding for x in its containing function, but its initializer is mutating the current x in scope, which happens to be the let-bound x. So the eval code is actually creating a variable binding two lexical frames out. The evaled var-declaration and its initializer are referring to two completely different bindings of x.

Lest you think this is strictly an unfortunate interaction between the legacy var and eval semantics and the new (post-ES3) let declaration form, there are a couple of other local declaration forms in pure ES3, including catch. Consider:
function test() {
var x = 'outer'
return (function() {
try { throw 'inner' }
catch(x) { eval("var x = 'eval'") }
return x
})()
}

4 comments:

Anonymous said...

Can you define let as syntactic sugar in terms of catch?

Dave Herman said...

I thought about this a while ago. You might be able to hack something together, but if it worked at all it would be very clunky. You need to work around the fact that the right-hand-sides need to be evaluated outside of the try-block, and also the problem that let is supposed to produce a value, but try/catch is only a statement form.

Anonymous said...

The following yields undefined:

function test() {
var x = 'outer'
return (function() {
try { throw 'inner' }
catch(x) { var x; x = 'catch'; }
return x
})()
}

While this yields "catch"
function test() {
var x = 'outer'
return (function() {
try { throw 'inner' }
catch(x) { var x = 'catch'; }
return x
})()
}

Unknown said...

Another interesting feature is of course: with. Then the var declaration could refer to one of the with scopes.

I take advantage of this in link.js to let all global variable bindings, defined in an eval:ed script, be isolated to a context - without wrapping the source.
https://github.com/sebmarkbage/link.js/blob/master/Source/Web/link.js#L622-L624

Interestingly, function declarations are hoisted out of a with statement, even when evaled. So, they can break out of scopes as well.

var x, scope = { x: 1 };
with (scope)​{
eval('function x(){}; x = 2;');
x = 3;
}
console.log(typeof x);​​​

That's why I have to do another eval to extract any function declarations that was made in the first one.