5

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.

  • It's not clear to me why you think that conflicts with the explanation. – jonrsharpe Dec 28 '19 at 16:41
  • When are you typing `foo` and getting `undefined`? The console is not a good place to check hoisting, since it can't know what you're going to type before you type it, but hoisting relies on the JavaScript engine pre-scanning the entire scope. – T.J. Crowder Dec 28 '19 at 16:44
  • @jonrsharpe There is no conflict of immediate result, that is as Kyle explains it. However, the subsequent inquiries into foo and foo(), that are in fact not discussed by Kyle, that was just my "curiosity/diligence :)" show that something else is happening eventually, that leads to subsequent results contradicting the results of the immediate execution. – Lucifer Morningstar Dec 28 '19 at 17:39
  • @jonrsharpe - Continued: Immediate execution: 1) foo is a function and gets executed as such, 2) var foo declaration is ignored. Subsequent result: foo is a (non-function) number 2 reference variable - so it clearly somehow got declared and assigned (not ignored) - foo() therefore throws and exception all the sudden. Is this a behavior intended by JavaScript language specification, or is this a bug? How about the V8 engine specification? Does anybody know? – Lucifer Morningstar Dec 28 '19 at 17:39
  • They don't contradict anything. Yes, after those lines the value assigned to foo is 2, which isn't callable, but that's not *"all of a sudden"*, or a bug. Maybe see e.g. https://stackoverflow.com/questions/15005098/why-does-javascript-hoist-variables. – jonrsharpe Dec 28 '19 at 17:42
  • @T.J.Crowder I had not typed foo and got undefined. I had input, (on 1 line): foo(); var foo = 2; function foo() { console.log("bar"); } function foo() { console.log("foo"); } and got the (2 line) result: foo < unefined (Sorry, but somebody edited the OP to make it "look nice" while overriding my original one-liner input to look like 4 lines, which of course would work perfectly fine if you use SHIFT+ENTER to input that code into console.) – Lucifer Morningstar Dec 28 '19 at 17:44
  • 1
    "*since `foo` is already declared as a function, the `var foo` declaration gets ignored by the (pre)compiler*" - that only refers to the `var` keyword, meaning it does not get declared again separately (or throws an error about redeclaring variables, like a `let` would). But when executing the code, the `foo = 2` initialisation still runs like an assignment. – Bergi Dec 28 '19 at 17:49
  • Everybody who attempts to answer this: Please, please do not talk here about the mythical hoisting. Please Talk about what is really happening on engine level, e.g.: 1) note and reserve memory for all the declared variables. 2) execute all executable code 3) missing misterious step ... – Lucifer Morningstar Dec 28 '19 at 17:53
  • @Bergi please change your Comment above into an Answer, so I can accept it. You may also add to it by referring to the relevant part of ECMAScript® 2019 Language Specification - ecma-international.org/publications/standards/Ecma-262.htm if you wish to do so. Thank you! – Lucifer Morningstar Dec 28 '19 at 19:14
  • @LuciferMorningstar - *'Please Talk about what is really happening on engine level..."* I've updated [my answer](https://stackoverflow.com/a/59513043/157247) to do that, with links to the relevant parts of the spec. – T.J. Crowder Dec 29 '19 at 12:17

6 Answers6

2

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);
Omri Attiya
  • 3,917
  • 3
  • 19
  • 35
  • Actually, paraphrasing Kyle, hoisting is just a "simplistic" explanation of the JavaScript engine's (pre) compilation step, that is widely accepted in the JavaScript community Mythology, to conveniently explain the (pre-compile) parse step, when all the declarations are noted and compiled away from the code BEFORE the actual execution. – Lucifer Morningstar Dec 28 '19 at 16:51
  • Also it is clear from the MDN Hoisting article, that it is in fact a mythical concept: "Hoisting was thought up as a general way of thinking about how execution contexts (specifically the creation and execution phases) work in JavaScript." – Lucifer Morningstar Dec 28 '19 at 17:59
2

Two answers for you:

  1. Why you're seeing undefined initially in the console, and

  2. 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

enter image description here

And then you're typing foo and getting 2:

> foo
< 2

enter image description here

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.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • thank you for walking us through the spec in such detail. I have a question regarding the handling of block-level variable statements in the `GlobalDeclarationInstantiation` (GDI) and `FunctionDeclarationInstantiation` (FDI) algorithms. Since step 7 of the GDI (and step 10 of the FDI) only identify [TopLevelVarScopedDeclarations](https://tc39.es/ecma262/#sec-static-semantics-varscopeddeclarations)), how do variable statements and function declarations inside blocks get "hoisted" to the enclosing global (or function scope, in case of the FDI)? – user51462 Jun 30 '21 at 05:27
  • @user51462 - Glad that helped! I don't think Step 6 (not 7, in the current spec) in GDI / Step 10 in FDI only identify top-level var scoped declarations. The spec operation used is [`VarScopedDeclarations`](https://tc39.es/ecma262/#sec-static-semantics-varscopeddeclarations), not [`TopLevelVarDeclaredNames`](https://tc39.es/ecma262/#sec-static-semantics-toplevelvardeclarednames). Perhaps I'm misunderstanding the question? – T.J. Crowder Jun 30 '21 at 06:46
  • Thank you for following up so quickly. I'm not sure, but the [SDT for `VarScopedDeclarations`](https://262.ecma-international.org/11.0/#sec-function-definitions-static-semantics-varscopeddeclarations) seems to imply that as it returns `TopLevelVarScopedDeclarations`. – user51462 Jun 30 '21 at 06:54
  • @user51462 - I'm afraid I have a couple of projects that have come to a head simultaneously, so I can't go on a spec deep-dive at the moment. Please let me know if you figure it out. :-) [This may help](https://timothygu.me/es-howto/) if you already already familiar with it. – T.J. Crowder Jun 30 '21 at 08:18
1

Nothing is getting ignored and the term "precompiled" is not really what you are seeing here. What you are experiencing is "hoisting".

All declarations (variable and function) get hoisted to the top of their enclosing block, so from an execution standpoint, the code is being processed as:

var foo;  // The declaration gets hoisted
function foo() { console.log("bar"); }
function foo() { console.log("foo"); }
foo();
foo = 2;  // But not the assignment

It's important to understand that only the declaration gets hoisted, but not the assignment, so the assignment of foo = 2 doesn't get hoisted and happens after everything else (see the "Only declarations are hoisted" section in the link I shared above).

As for you seeing undefined in your console, that's just the console's way of saying that what you've executed has no return value (not the same thing as result). A declaration statement of var foo doesn't have a return value or result in a value, so the console shows you undefined. If you were to simply type: var x = 10; into your console, you'd see the same undefined response.

Scott Marcus
  • 64,069
  • 6
  • 49
  • 71
  • Actually, your bold point might be what the OP's confused about, I can't tell. – T.J. Crowder Dec 28 '19 at 16:46
  • Actually, paraphrasing Kyle, hoisting is just a "simplistic" explanation of the JavaScript engine's (pre) compilation step, that is widely accepted in the JavaScript community Mythology, to conveniently explain the (pre-compile) parse step, when all the declarations are noted and compiled away from the code BEFORE the actual execution. – Lucifer Morningstar Dec 28 '19 at 16:53
  • @LuciferMorningstar The internal workings of how a client processes and optimizes JavaScript is actually up to the client to decide, so the term "pre-compile" isn't guaranteed to be 100% accurate. But, what happens during that client implementation is what matters and "hoisting" is what you are seeing here. – Scott Marcus Dec 28 '19 at 16:55
  • *"As for you seeing undefined in your console, that's just the console's way of saying that what you've executed has no return value..."* Not quite. Most consoles (at least in browsers) show the *result* of statements, even though you can't use the result of statements in code. For instance, try: `{ "foo"; }`, which is a block statement with a single expression statement in it. Most consoles will show you the result of that block statement (`"foo"`), even though you can't use those results in code. In this case I suspect it's the result of the `var` statement. :-) – T.J. Crowder Dec 28 '19 at 17:06
  • Also it is clear from the MDN Hoisting article, that it is in fact a mythical concept: "Hoisting was thought up as a general way of thinking about how execution contexts (specifically the creation and execution phases) work in JavaScript." However, in fact, in what order are the variables created (and possibly overridden) in the creation phase? – Lucifer Morningstar Dec 28 '19 at 18:02
  • @LuciferMorningstar You are getting a bit too caught up in terminology. If you are truly interested in learning how this stuff works, you may want to consider using the terminology that is commonplace. What you are asking about is answered by understanding the concept of hoisting. Whether that term best describes the underlying computing that is taking place in different implementations is irrelevant to your question. There is a reason that documentation on "hoisting" on the website of the organization that is the steward of JavaScript exists. – Scott Marcus Dec 28 '19 at 18:21
  • @LuciferMorningstar The term "hoisting" is actually mentioned in the [ECMAScript specification](https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf), so the concept is less mythological than you believe. – Scott Marcus Dec 28 '19 at 18:26
  • @ScottMarcus Yes, it is unfortunately used there in 2 places, however, IMHO, it should go the way of Dodo, or more relevantly the way of of SEAF “self-executing anonymous function” (or SIAF self-invoked anonymous function) that is now luckily called IIFY (Immediately-Invoked Function Expression), since it is the description of the reality. I would suggest to ECMA to replace it, or you can, if you agree. I like the MDN explanation of this myth: https://developer.mozilla.org/en-US/docs/Glossary/Hoisting – Lucifer Morningstar Dec 28 '19 at 20:01
  • And I would totally agree with the bold parts, if they did not use the mythical hoisting terminology. Accepted in general use does not mean clear or best, as per the MDN glossary: developer.mozilla.org/en-US/docs/Glossary/Hoisting – Lucifer Morningstar Dec 28 '19 at 20:07
  • @LuciferMorningstar I think now you are just being contrarian for the sake of it. The bold parts are facts. Hoisting is not mythology, it's a real effect and a real term, hence the reason it has an entry in MDN and is mentioned in the actual ECMAScript specification in the first place. You misunderstand the term's significance. It's simply a byproduct of a larger process, but that doesn't make it any less real than the larger process. And, it answers your question. – Scott Marcus Dec 28 '19 at 20:24
  • @ScottMarcus - you are absolutely right. I misunderstand the term's significance, since I am a clarity junkie. I just prefer the (to me) cleaner **Compilation phase**, or even the MDN's **Creation phase** to the **hoisting** term, especially, because in reality the code is not getting moved around, like hoisting apearantly misleads many people to believe, it is just getting partially compiled away in the first phase. – Lucifer Morningstar Dec 28 '19 at 22:08
  • @LuciferMorningstar You are confusing the phase with a side-effect of what happens during that phase - the terms are not interchangeable. In reality, the point is moot. Hoisting is the net result and is what you are going to find people refer to. And again, this side-effect is the answer to the question you posed. – Scott Marcus Dec 28 '19 at 22:11
  • @ScottMarcus Quoting Kyle Simpson: "There is this conceptual model for how JavaScript works, and that conceptual model is called hoisting. As I will suggest to you, it's **not really physically what actually happens**. If you open up the spec, you'll not find the word hoisting anywhere, because hoisting isn't actually a thing. It's a mental construct that we have invented to explain the behaviors of JavaScript. " Also please note that the 2 times the term hoisting is even mentioned in the spec only got there since the ECMASCRIPT 2015 proposal. I would ask them to remove it. – Lucifer Morningstar Dec 29 '19 at 00:05
  • @LuciferMorningstar ... In other words, as I've already said, you are arguing semantics that are besides the actual point. Your question has been answered. – Scott Marcus Dec 29 '19 at 04:14
0

Since variable and function declarations get hoisted to the top of the code, your piece of code is semantically equivalent to the following:

var foo;
 
function foo() {
  console.log("bar");
}

function foo() {
  console.log("foo");
}

foo();
foo = 2;

The second function declaration indeed overrides the first one, and that's why you get "foo" printed on the console when calling foo().

The last statement is simply overriding foo once again with 2 instead of a function. It's not being ignored, as foo after this holds the value 2; it's just being evaluated for last.

Alberto Trindade Tavares
  • 10,056
  • 5
  • 38
  • 46
  • 3
    More accurately: `var foo; function foo() { /*...*/ } function foo() { /*...*/ } foo(); foo = 2;` (But the main point of the question seems to be about doing things in the console, which isn't a good environment for understanding this stuff.) – T.J. Crowder Dec 28 '19 at 16:42
  • Actually, paraphrasing Kyle, hoisting is just a "simplistic" explanation of the JavaScript engine's (pre) compilation step, that is widely accepted in the JavaScript community Mythology, to conveniently explain the (pre-compile) parse step, when all the declarations are noted and compiled away from the code BEFORE the actual execution. – Lucifer Morningstar Dec 28 '19 at 16:53
  • @T.J.Crowder In my answer I commented you that it's actually behaves the same way it behaves in the console. – Omri Attiya Dec 28 '19 at 16:53
  • Also it is clear from the MDN Hoisting article, that it is in fact a mythical concept: "Hoisting was thought up as a general way of thinking about how execution contexts (specifically the creation and execution phases) work in JavaScript." However, in fact, in what order are the variables created (and possibly overridden) in the creation phase? – Lucifer Morningstar Dec 28 '19 at 18:01
0

OK, so a comment by @Bergi to the OP clearly explains the behavior I was asking about, and I had asked him to create an answer from it, so I can mark it as accepted. In meantime, here is the explanation of the immediate output, as well as subsequent inquiries, as I now understand it, based on this discussion:

1) In the JavaScript ** creation of the variables/initialisation of the scope** (aka (conceptual/mythical) hoisting) phase a reference foo is declared and then overridden as a function, and the subsequent var foo declaration is ignored.

2) In the subsequent JavaScript execution phase:

  • a) foo(); is executed, and at that time foo is a function, so the result foo is output to the first line as per console.log("foo");

  • b) foo = 2; is executed, redefining foo from function to a number and assigning 2 to it, while outputting undefined as the result of that whole operation.

That is it, and can be also hopefully divined by actually reading the 15.1.11 Runtime Semantics: GlobalDeclarationInstantiation ( script, env ) - http://ecma-international.org/ecma-262/10.0/index.html#sec-globaldeclarationinstantiation part of the standard.

Community
  • 1
  • 1
  • @Bergi please look at this Self-Answer I had based on your comment to the OP, as well as on parts of the other answers, that mostly unfortunately insist on the hoisting terminology that I abhor of, and copy/edit/expand it in your answer according to your best knowledge, so I can accept it. – Lucifer Morningstar Dec 28 '19 at 20:14
  • I still don't understand what's the difference between this answer and the rest of the answers – Omri Attiya Dec 29 '19 at 06:47
  • 1
    Looks good to me, except for calling it "compilation phase" - it really is just the creation of the variables/initialisation of the scope :-) See @TJCrowder's answer for more details and the spec references – Bergi Dec 29 '19 at 13:57
  • *"...while outputting `undefined` as the result of that whole operation."* *Sort* of. It's the result of the `var foo = 2;` VarStatement, which is the last thing executed in the code, and so it's the last result in the code and it's the one reported by the console. – T.J. Crowder Dec 31 '19 at 09:03
-1

First the function definitions get precompiled, then the interpreter will start the execution of the other statements. var foo = 5 will override the function - except if there is "use strict", which will throw an error if you try to redefine a name.

CoderCharmander
  • 1,862
  • 10
  • 18