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);
  • Also see [Are variables declared with let or const hoisted?](https://stackoverflow.com/questions/31219420/are-variables-declared-with-let-or-const-hoisted) – showdev Jul 04 '21 at 09:23

1 Answers1

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.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Ok. Any good resource to read this up? – Tushar Shahi Jul 04 '21 at 09:30
  • 1
    @TusharShahi - At the risk of self-promotion, Chapter 2 of my recent book *JavaScript: The New Toys* covers this in detail; links [here](https://thenewtoys.dev). ;-) – T.J. Crowder Jul 04 '21 at 09:49
  • 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 Jul 06 '21 at 04:17
  • @NewbieToCoding - Because, as I said in the answer, the *initialization* of the binding is not hoisted, and you can't use an uninitialized binding. – T.J. Crowder Jul 06 '21 at 06:46
  • 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? – NewbieToCoding Jul 06 '21 at 08:21
  • @NewbieToCoding - I think the two meanings of "initialization" are confusing you here (they certainly confused me when I first got into this). I've added to the end of the answer to try to explain it further with a slight change in terminology. – T.J. Crowder Jul 06 '21 at 09:59
  • *The function declaration (function foo() { }) creates a binding called foo with its usable flag set to true (can be used) and its value set to undefined.* But it is not undefined right? foo is not undefined and can be called. Please correct me – Tushar Shahi Jul 09 '21 at 07:16
  • @TusharShahi - See step 1.2 in that list. :-) (The spec does it in the way shown above, but it's impossible to see the `undefined` value of `foo` between step 1.1 and 1.2 so it's absolutely fine to think of it has the binding is created with the function as its initial value. I wanted to be true to the spec in the list, but pragmatically it's as though the binding were created with the function value to start with.) – T.J. Crowder Jul 09 '21 at 07:52