I thought that const and let were hoisted just like var, but this leads me to believe I am wrong.
Not really, let
and const
are indeed hoisted, or as I like to call it, half-hoisted. There are two big differences between var foo
and let foo
: Scope and initialization. You already know about the scope difference. The second one is that with var foo
, both the declaration and the initialization (with undefined
) of foo
are hoisted. With let
, only the declaration of foo
is hoisted, not the initialization. foo
is only initialized when the step-by-step execution of the code reaches the let foo
statement. You can't use (read from or write to) an un-initialized identifier. This time during which you can't use the identifier is called the Temporal Dead Zone (TDZ).
Even with var
, the initialization that's hoisted is the initialization with undefined
, not the value on the right-hand side of the =
:
console.log(typeof foo); // "undefined"
foo(); // TypeError: foo is not a function
var foo = () => {};
The change you've made, moving the declaration of getRandom
up before the first use of it, is the correct thing to do. (Or use a function declaration, since the declaration as a whole [including the creation of the function] is hoisted.)
Let's look at this half-hoisting thing:
let foo = "outer";
function x()
{
console.log("...");
let foo = "inner";
console.log(foo);
}
x();
(let
and const
have block scope, but I'm using a function because I'll be constrasting with var
in a moment.)
Within x
, that inner foo
can't be used until the let foo
line. But, you can't access the outer foo
above it; this fails:
let foo = "outer";
function x()
{
console.log(foo); // ReferenceError: `foo` is not defined
let foo = "inner";
console.log(foo);
}
x();
That's the half-hoisting: The declaration of the inner foo
is hoisted, but the variable isn't initialized until the let foo
statement. That means you can't use foo
at all (not even from a containing scope) above the let foo
line. The inner foo
shadows the outer foo
throughout the function, but you can't use it until it's initialized. This is covered in Let and Const Declarations in the spec.
This is in constrast with var
:
var foo = "outer";
function x()
{
console.log(foo); // undefined
var foo = "inner";
console.log(foo); // "inner"
}
x();
That runs just fine because both the declaration and the initialization of foo
(with undefined
) are hoisted to the top of the function. (After hoisting, the var foo = "inner";
line becomes a simple assignment statement.) So the inner foo
shadows the outer foo
throughout, and is also accessible throughout, initially with its default value (undefined
) and the later with "inner"
(once that's been assigned to it).
Since the TDZ is temporal (related to time), not spatial (related to space or location within the scope), you can use an identifier created by let
or const
(or class
) above its declaration, just not before its declaration. This fails because getNumber
tries to access theNumber
before it's initialized, while it's still in the TDZ:
const getNumber = () => theNumber;
console.log(getNumber()); // ReferenceError
let theNumber = 42;
This works because getNumber
accesses theNumber
after it's initialized:
const getNumber = () => theNumber;
let theNumber = 42;
console.log(getNumber()); // 42