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:
When the execution context for the script is created:
- 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
.
- 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.
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
.
When foo
is called and the execution context for the call is created, bindings for top-level declarations are created:
- A binding called
a
is created by let a = 3;
with its usable
flag set to false
(can't be used yet).
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.
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:
- "Initializing" the binding (making it usable, setting
usable
to true
), and separately
- "Initializing" as in setting the initial value of a binding.
They are separate things.