3

I've read on multiple websites (like w3schools) that hoisting is "the behavior of moving all declarations to the top of the current scope".

For let and const, the variables are hoisted but not initialized.

I understand why the following code does not work as name has no value for us to access.

console.log(name);
let name = "hi";

But why can't we assign a value to name before its actual declaration, even though name has been declared (hoisting)?

name = "hi";
let name;
console.log(name);

Isn't the above code the same as the following,

let name;
name = "hi";
console.log(name);

1

1 Answer 1

7

Because they're explicitly designed not to allow that, because it's usually a programming mistake.

let and const are hoisted, but it's just the declaration of the binding that's hoisted. (Loosely, "binding" means "variable" [or constant or parameter...things with names we use to hold values].) The binding is not initialized until later, when the let or const statement is reached in the step-by-step execution of the code. You can't use an uninitialized binding (in any way), which is why you get an error.

In contrast, with var both declaration and initialization are hoisted; var bindings are initialized with the value undefined. If there's an initialization value on the var (var a = 42), later when the var statement is reached in the step-by-step execution of the code, that part is treated as simple assignment (a = 42). With let and const, it's not just simple assignment, it's initialization of the binding, allowing it to be used.

Here's a concrete example of how let hoists the declaration but not the initialization, and why it helps prevent programming mistakes:

let a = 1;

function foo() {
    a = 2;  // <=== Which `a` should be assigned to?
    console.log(a);
    
    // code
    // code
    // code
    // code
    // code
    // code
    // code
    // code

    let a = 3;
    console.log(a);
}

foo();

In that code, it seems like the assignment at the top of foo should assign to the outer a, since (as far as we know reading top-down) there's no other a in scope. But there is, because the let at the bottom of foo is hoisted. You get an error doing the assignment because that inner a isn't initialized.

In contrast, with var, there's no error but it's easy to be confused about which a was assigned at the top of foo.


In comments you've indicated you're still not understanding what it means for a binding to be declared but not initialized. I think the two (slightly) meanings of "initialization"¹ are confusing you here (they confused me when I got into this stuff), so let's change terminology slightly.

Bindings have a flag associated with them saying whether they can be used or not. Let's call it the usable flag: usable = true means the binding can be used, usable = false means it can't. Using that terminology, the example above is handled like this:

  1. When the execution context for the script is created:

    1. Bindings for all top level declarations within it are created:
      • The let a part of let a = 1; creates a binding called a with its usable flag set to false (can't be used yet).
      • The function declaration (function foo() { }) creates a binding called foo with its usable flag set to true (can b eused) and its value set to undefined.
    2. Function declarations within the context are processed by creating the functions they define and assigning them to the binding. So foo gets its function value.
  2. When the let a = 1; statement is encountered in the step-by-step execution of the code, it does two things: It sets the usable flag to true (can be used) and sets the value of a to 1.

  3. When foo is called and the execution context for the call is created, bindings for top-level declarations are created:

    1. A binding called a is created by let a = 3; with its usable flag set to false (can't be used yet).
  4. When the a = 2; statement is reached in the step-by-step execution of the code, the a resolves to the inner a binding (the one in foo, declared by let a = 3;), but that binding's usable flag is false, so trying to use it throws an error.

  5. If we didn't have the a = 2; statement, so no error was thrown, then when the step-by-step code execution reached the let a = 3; statement, it would do two things: Sets the usable flag to true (can be used) and set the value of a to 3.

Here's foo updated with some comments:

function foo() {
    // The local `a` is created but marked `usable` = `false`

    a = 2;      // <=== Throws error because `a`'s `usable` is `false`
    console.log(a);
    
    let a = 3;  // <=== If there weren't an error above, this would set
                //      `usable` to `true` and the value of `a` to `3`
    console.log(a);
}

¹ "I think the two (slightly) meanings of "initialization"¹ are confusing you here..." The two meanings I'm referring to are:

  1. "Initializing" the binding (making it usable, setting usable to true), and separately
  2. "Initializing" as in setting the initial value of a binding.

They are separate things.

Sign up to request clarification or add additional context in comments.

8 Comments

Ok. Any good resource to read this up?
@TusharShahi - At the risk of self-promotion, Chapter 2 of my recent book JavaScript: The New Toys covers this in detail; links here. ;-)
If the declaration of let a = 3 is hoisted to the top the of the function, why doesn't the program just output 2 followed by 3?
@NewbieToCoding - Because, as I said in the answer, the initialization of the binding is not hoisted, and you can't use an uninitialized binding.
But how is let a; a = 3; different from a = 3; let a; if in the second case, the declaration is hoisted such that it becomes the same as the first code? Why does the second code produce an error?
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.