Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why and when does an immediately ignored variable declaration take hold, and overrides a function declaration in subsequent inquiries?

Tags:

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.

like image 672
Lucifer Morningstar Avatar asked Dec 28 '19 16:12

Lucifer Morningstar


2 Answers

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);
like image 69
Omri Attiya Avatar answered Nov 15 '22 04:11

Omri Attiya


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.

like image 32
T.J. Crowder Avatar answered Nov 15 '22 06:11

T.J. Crowder