While following Advanced JavaScript by Kyle Simpson on Pluralsight, I came across this piece of code that is supposed to prove that function declarations get (pre)compiled before variable declarations:
foo();
var foo = 2;
function foo() { console.log("bar"); }
function foo() { console.log("foo"); }
(Please note that the above code should be either entered as a single line with spaces instead of line breaks, or use SHIFT+ENTER to prevent immediate execution before the entire code is entered.) The immediate result of entering the entire code above in Node or (Chrome) console (and hitting the Enter key) is:
foo
< undefined
Paraphrasing Kyle's explanation, the first function foo
declaration gets overridden by the second, so foo
gets output to console and, since foo
is already declared as a function, the var foo
declaration gets ignored by the (pre)compiler.
The immediate result supports the ignored theory, however, subsequent inquiry into foo
and foo()
shows a different story:
> foo
2
> foo()
Uncaught TypeError: foo is not a function
>
Can somebody please explain why and when the ignored declaration of var foo = 2;
is taking hold, when the immediate execution produces:
foo
< undefined
My understanding was that the JavaScript engine (pre)compile parsing step should note the declarations of the two functions and then of the variable, in that order, and then, at the execution step the foo();
execution attempt should fail as it does subsequently, with: Uncaught TypeError: foo is not a function
- this however clearly is not the case, since foo
gets output as a part of the immediate result.
Variables and functions in javascript are Hoisting
As written here:
Hoisting is a JavaScript mechanism where variables and function declarations are moved to the top of their scope before code execution. Inevitably, this means that no matter where functions and variables are declared, they are moved to the top of their scope regardless of whether their scope is global or local
Which means that after you write this:
foo();
var foo = 2;
function foo() {
console.log("bar");
}
function foo() {
console.log("foo");
}
It actually getting looks like this (which is very logic of why things happens the way they are:
var foo;
function foo() {
console.log("bar");
}
function foo() {
console.log("foo");
}
foo(); // this happens first
foo = 2; // this happens after
console.log(foo);
Two answers for you:
Why you're seeing undefined
initially in the console, and
What's going on in that code in detail
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.
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):
var
declarations and function declarations. In your example, varDeclarations will include:
foo
(the var
variable),foo
(the first foo
function), andfoo
(the second foo
function)var
declaration (or a couple of other similar bindings), this loop ignores it because this loop only cares about functions.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.
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.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"
.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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With