11

Consider the following code:

function f() {
    f = eval("" + f);
    console.log("Inside a call to f(), f is: \n%s", f);
}

f();

console.log("After a call to f(), f is: \n%s", f);

I expected f to be defined at all times during execution. However, in Chrome and IE, it is undefined when the first console.log is invoked, and in Firefox, it is undefined when the second console.log is invoked.

Why is f not always defined? Why do Chrome/IE and Firefox behave differently?

http://jsfiddle.net/G2Q2g/

Output on Firefox 26:

Inside a call to f(), f is:

function f() {
    f = eval("" + f);
    console.log("Inside a call to f(), f is: \n%s", f);
}

After a call to f(), f is:

undefined

Output on Chrome 31 and IE 11:

Inside a call to f(), f is:

undefined

After a call to f(), f is:

function f() {
    f = eval("" + f);
    console.log("Inside a call to f(), f is: \n%s", f);
}
user247702
  • 23,641
  • 15
  • 110
  • 157
Dagg Nabbit
  • 75,346
  • 19
  • 113
  • 141
  • Suggestions for a better title welcome :) – Dagg Nabbit Jan 08 '14 at 22:56
  • Maybe their consoles are made differently..? – Cilan Jan 08 '14 at 22:57
  • Hoisting works differently in Spidermonkey and V8 – adeneo Jan 08 '14 at 22:59
  • @ManofSnow, http://jsfiddle.net/G2Q2g/1/ – Dagg Nabbit Jan 08 '14 at 22:59
  • In case anyone cares - in IE the first call is undefined and the second is the function - just like in Chrome. – Benjamin Gruenbaum Jan 08 '14 at 23:01
  • @adeneo how so? (I personally would bet on `Function.prototype.toString` being different on the console by the way) – Benjamin Gruenbaum Jan 08 '14 at 23:03
  • @BenjaminGruenbaum - Looks to me like it's related to the same issues Firefox has with function expressions in conditions, but I could be wrong ? – adeneo Jan 08 '14 at 23:04
  • The thing that's really bothering me is why it isn't defined in both places in all browsers. I don't think it has anything to do with the console, but could be wrong (see my reply to Man of Snow) – Dagg Nabbit Jan 08 '14 at 23:05
  • @DaggNabbit - the console just logs, if it's undefined, it's undefined, and something is moving in a different way in firefox, and it's generally hoisting that moves functions and variables around. – adeneo Jan 08 '14 at 23:07
  • 1
    @adeneo this definitely has something to do with the `eval` because when we do not perform the `""` inside the eval - it works. Also, when we use `new Function` or assign it to itself - it works. – Benjamin Gruenbaum Jan 08 '14 at 23:09
  • 1
    If you do something other than eval'ing, it works as expected -> http://jsfiddle.net/G2Q2g/2/ – adeneo Jan 08 '14 at 23:09
  • @adeneo, lots of things work as expected; I'm trying to figure out why this doesn't ;) – Dagg Nabbit Jan 08 '14 at 23:10
  • That happens only on JSFiddle. Try to run that code on chrome console. You will get the same output as Firefox. Didn't check it on Internet explorer though. I don't know why its a different output in JSFiddle. Maybe the way they handle javascript. – emphaticsunshine Jan 08 '14 at 23:12
  • Scratch that, of course if we remove the `""` it works, if the parameter of `eval` is not a string it just returns that parameter. `.toString` produces the same issue. @emphaticsunshine I'm in the Chrome console and am not getting the same output as FF. – Benjamin Gruenbaum Jan 08 '14 at 23:12
  • @emphaticsunshine, what version of Chrome? It works as described here in my console. – Dagg Nabbit Jan 08 '14 at 23:13
  • If you create a second function and put this one inside of eval, then both console.logs will show `undefined`: http://jsfiddle.net/K54h4/ – basilikum Jan 08 '14 at 23:14
  • Note - in strict mode it returns `undefined` in all browsers both times. @ManofSnow if you pass anything but a string to eval - it'll just return it. Also, `alert` does not support varargs. – Benjamin Gruenbaum Jan 08 '14 at 23:14
  • Version 31.0.1650.63 on Mac OSX 10.8.5 – emphaticsunshine Jan 08 '14 at 23:15
  • It just seems somewhat similar to this -> **http://jsfiddle.net/G2Q2g/6/**, where firefox is the only browser that returns true, all other browsers will return false, even if you would expect it to return true. – adeneo Jan 08 '14 at 23:21
  • @adeneo right, but that example is invalid JavaScript, to be fair I'd expect it not to compile - the semantics of putting functions in `if` statements were never written it's not legal ECMAScript, the code above on the other hand is just ambiguous in the specification. – Benjamin Gruenbaum Jan 08 '14 at 23:32
  • @adeneo's example is valid in Firefox because Firefox gives definition that syntax. Though it would be correct to say that it's invalid ECMAScript. – cookie monster Jan 08 '14 at 23:35
  • 1
    I asked in es-discuss now http://esdiscuss.org/topic/behavior-of-eval-in-non-strict-mode – Benjamin Gruenbaum Jan 09 '14 at 00:03

3 Answers3

8

First of all, let's talk about what we would 'expect'.

I would naively expect both cases to return undefined.

  • Just like: eval("function foo(){}") which returns undefined.

  • Just like whenever we have a function declaration - it does not return the function value but sets it.

  • Just like the langue specification says for strict mode.

Update: after digging more through the spec - Firefox is correct here.

Here is what Firefox is doing

Visualized:

  1. f = eval("" + f); // set the left hand side to the function f we're in
  2. f = eval("" + f); // declare a new function f in the scope of this function
  3. f = undefined; // since undefined === eval("function(){}"); *

* since function declarations do not return anything - just like function foo(){} has no return value

Since f was decided in step 1, right now the reference to the function we're in was overwritten with undefined and a local closure declared f was declared with the same code. Now when we do:

console.log("Inside a call to f(), f is: \n%s", f) // f is the local closure variable, it's closest

Suddenly, it's obvious we get the function - it's a member variable.

However, as soon as we escape the function

console.log("After a call to f(), f is: \n%s", f);

Here, f is undefined since we overwrote it in step 1.

Chrome and IE make the mistake of assigning it to the wrong f and evaluating the right hand side before the left hand side of an assignment.

Why it works in strict mode

Note that the next section says in Entering eval code:

Let strictVarEnv be the result of calling NewDeclarativeEnvironment passing the LexicalEnvironment as the argument.

Which explains why it works in strict mode - it's all run in a new context.


Same thing, but in more text and less graphically

  • "Find" f from f = (since the left hand side must be evaluated first. This refers to the local copy of f. That is, evaluate the left hand side first.
  • Perform the eval call which returns undefined but declares a new local function of f.
  • Since f from f = was evaluated before the the function itself, when we assign undefined to it we're actually replacing the global function
  • So when we do console.log inside we're referring to the local copy declared in the eval since it's closer in the scope chain.
  • When we're on the outside and do console.log, we are now referring to the 'global' f which we assigned undefined to.

The trick is, the f we're assigning to and the f that we're logging are two different fs. This is because the left hand side of an assignment is always evaluated first (sec 11.13.1 in the spec).

IE and Chrome make the mistake of assigning to the local f. Which is clearly incorrect since the specification clearly tells us:

  1. Let lref be the result of evaluating LeftHandSideExpression.

  2. Let rref be the result of evaluating AssignmentExpression.

So, as we cal see the lref needs to be evaluated first.

(link to relevant esdiscuss thread)

Community
  • 1
  • 1
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • If chrome is defining `f` on the global scope, then why is there no `window.f`? – cookie monster Jan 08 '14 at 23:45
  • @cookiemonster I'm getting `window.f` in my browser window (which figures, otherwise the second `.log` would not work). By the way https://hg.mozilla.org/mozilla-central/file/b9ca7a6f891a/js/src/builtin/Eval.cpp#l333 in case you want to investigate (this is where I'm currently looking for answers) – Benjamin Gruenbaum Jan 08 '14 at 23:46
  • @cookiemonster that's because you introduced another scope - JSFiddle wraps the call in an `onload` handler so eval puts the function in _that_ scope . Here - http://jsfiddle.net/zHvTB/ . – Benjamin Gruenbaum Jan 08 '14 at 23:51
  • Yes, but that's the same as the jsFiddle in the question. I assumed your answer was based off that example. – cookie monster Jan 09 '14 at 00:23
  • @BenjaminGruenbaum from the last comment on your esdiscuss topic ("The left hand side of an assignment is always evaluated before the right hand side. This includes resolving and remembering the reference information for an identifier reference") and the second example in [#1751](https://bugs.ecmascript.org/show_bug.cgi?id=1751), it's starting to look like Chrome messed up and Firefox is right, isn't it? – Dagg Nabbit Jan 09 '14 at 19:20
  • @DaggNabbit Note that the example from the bug is assignment where in our case a function declaration is performed. The difference is not which part is evaluated first but what scope the whole `eval` is run in. This question is very similar and might similar, but it's different and more about context. To be completely fair I believe the spec is ambiguous here and I'll wait for a bit longer before I bug more people in the thread there - my hope is to make this clearer and less ambiguous in the coming ES6 spec. – Benjamin Gruenbaum Jan 09 '14 at 19:26
  • @BenjaminGruenbaum I'm not sure it's possible to tell what's causing it without looking at some source code, but assume the `eval` always runs in the scope of `f`, so in each case a new `f` is declared inside of `f`, but in Firefox, the lhs (`f = `) still refers to the global scope (as it should according to that comment) so the global `f` is assigned `undefined`, while in Chrome, the lhs refers to the newly-declared inner `f`, so the inner `f` is `undefined`. I might be missing something but this seems at least as likely a culprit as the eval scope. – Dagg Nabbit Jan 09 '14 at 20:00
  • @DaggNabbit my old answer was incorrect - I've updated it to reflect what I think describes the problem a lot better. – Benjamin Gruenbaum Jan 11 '14 at 02:01
  • @BenjaminGruenbaum, yeah, this makes sense. I'm sort of curious what "no calling context" means now too, even if it wasn't related. Might have to write a question about that next. – Dagg Nabbit Jan 11 '14 at 05:26
2

I'm sorry but I can only answer your first question as of now :-/

I expected f to be defined at all times during execution. Why is f not always defined?

Two things:

  • evaling a function declaration does return undefined. It may be redefined as it is evaluated, but you then assign undefined to f thereafter.
  • f is a local variable in the function f, since named functions are available in their own scopes.

Check this behaviour in http://jsfiddle.net/G2Q2g/5/.

So now you at least may ask

Why is it undefined when the second console.log is invoked in Firefox, as opposed to the correct behaviour in Opera, Chrome and IE?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Still I'm not sure what goes wrong in firefox. Is it the evaled function statement that does open the scope of the local `f`? – Bergi Jan 08 '14 at 23:23
  • I'm not sure either, still trying to wrap my head around that part of BG's answer. – Dagg Nabbit Jan 09 '14 at 00:06
0

Change your code to

function f() {
    f = eval("(" + f + ")");
    console.log("Inside a call to f(), f is: \n%s", f);
};

f();

console.log("After a call to f(), f is: \n%s", f);

and it will work. Note that function source is wrapped into "(" ")" brackets to make it an lvalue expression and not a declaration.

Here it is http://jsfiddle.net/G2Q2g/8/

UPDATE:

eval(str) parses and executes the string as a Program (ECMAScript term). Function declaration by itself is a declaration, it has no value. But this ( func() ... ) is an expression that has value. And eval returns value of last expression. That' s what you see here.

c-smile
  • 26,734
  • 7
  • 59
  • 86