Two answers for you:
Why you're seeing undefined
initially in the console, and
What's going on in that code in detail
Why you're seeing undefined
If you mean you're copying the entire thing and pasting it all at once into the console, like this:
> foo();
var foo = 2;
function foo() { console.log("bar"); }
function foo() { console.log("foo"); }
And you're seeing the output foo
and undefined
like this:
foo
< undefined

And then you're typing foo
and getting 2
:
> foo
< 2

The reason for the first undefined
is that it's the result of the var
statement; statements have result values even though you can't use them in code, and consoles often show you those result values. Try just:
var foo = 2;
and you'll also see undefined
as the result.
That undefined
has nothing to do with hoisting.
I do strongly encourage you not to use the console for testing things related to hoisting. Consoles are unusual environments. If you want to test something like that, use a script and the debugger built into your IDE and/or browser instead, setting breakpoints and examining the contents of the scope at the times you're interested in.
What's going on in that code in detail
Regarding what this code is actually doing:
foo();
var foo = 2;
function foo() { console.log("bar"); }
function foo() { console.log("foo"); }
Assuming this is at global scope, you start in the spec at InitializeHostDefinedRealm, which creates the global execution context and uses SetRealmGlobalObject to create the global environment object (not the global object, the browser provides that), etc. (In a comment somewhere you said people should explain where memory is reserved and such. The specification leaves that up to implementations, but creating those environment objects is vaguely close.) Then you'd pick up again at ScriptEvaluation for the script containing that code, which calls GlobalDeclarationInstantiation which contains the meat of what you seem interested in. (If that code were within a function, you'd start at the standard [[Call]] operation, follow it to EvaluateBody for functions, and then you'd find the meat in FunctionDeclarationInstantiation, which does fundamentally the same thing in relation to your code that GlobalDeclarationInstantiation does.)
Looking at GlobalDeclarationInstantiation (the step numbers will slowly rot as the spec evolves; the names of things are usually fairly stable though):
- In Step 7, the engine builds a list (varDeclarations) of "VarScopedDeclarations" in the top level of the script, which includes both
var
declarations and function declarations. In your example, varDeclarations will include:
foo
(the var
variable),
foo
(the first foo
function), and
foo
(the second foo
function)
- In Step 8, it creates a blank list of functions to initialize, functionsToInitialize.
- In Step 9, it creates a blank list of declared function names, declaredFunctionNames.
- In Step 10, it goes through varDeclarations in reverse order:
- If the entry is a
var
declaration (or a couple of other similar bindings), this loop ignores it because this loop only cares about functions.
- If the entry is for a function:
- If the function's name isn't in declaredFunctionNames, the engine declares the function, adds the name to declaredFunctionNames, and inserts the function into functionsToInitialize at the beginning.
In your code, the engine starts with the second foo
function declaration (the last item in varDeclarations), doesn't see foo
in declaredFunctionNames, so it declares the function and puts it on the list to initialize. On the second pass, foo
is already on the list so the engine doesn't do anything with it. On the third pass, the foo
is a var
declaration so it doesn't do anything with it.
- In Step 11 it creates a blank list of declaredVarNames.
- In Step 12 it loops through varDeclarations again, this time looking only at
var
declarations and not the two function declarations in the list. If the var
name isn't in declaredFunctionNames, the engine creates a global variable. But in your code, foo
is in declaredFunctionNames, so it's skipped.
- In Step 17 the engine loops through functionsToInitialize, initializing them. In your code, this initializes the second
foo
function, which was put on the list in Step 10 (the fourth bullet in this list).
At this point, the function foo
(the second one) has been created and associated with the global binding for "foo"
and the var foo
part of var foo = 2;
has been skipped, since the var
was superceded by a function declaration. Now it's time to evaluate the body of that code:
foo()
calls the second foo
function, which outputs "foo"
.
- The
var foo = 2;
VariableStatement is evaluated. The var
part, of course, has already been done (skipped, in this case), so it's just the foo = 2
part that's evaluated at this point, assigning the value 2
to the global "foo"
binding. The result of VariableStatement is an empty completion, which the console shows as undefined
(even though you can't use that result in code). (If you want to prove to yourself that the undefined
is coming from the VariableStatement, just remove the var
and paste the result into the console. You'll see 2
[the result of the assignment statement] instead of undefined
.)
Once the code has finished running, the "foo"
global binding's value is 2
, because the initialization in the var
statement overwrote the function that used to be assigned to the binding.
Looking at the bigger picture, if we remove the things that will be ignored or skipped by the JavaScript engine, and if we reorder them so they're listed in the order in which they occur, that code is functionally identical to:
function foo() { console.log("foo"); }
foo();
foo = 2;
...except, of course, for the fact that the assignment statement foo = 2;
results in 2
, whereas the variable statement var foo = 2;
results in undefined
. You can only see that difference in a console or similar, though, not in code.