Though in JavaScript block-scoped const and let declarations aren’t hoisted to the top of the function, they are still hoisted to the top of the block. As such, once a block is entered, all bindings scoped to that block immediately become available, even those that have not yet been initialized. Accessing a binding between entering its scope and initializing it is a runtime error: the time when this can happen is known as the temporal dead zone. The expression initializing a block-scoped binding is within its temporal dead zone; as such, using the binding identifier in its initializing expression will throw an error.
Although I also sometimes find this design decision disappointing, there are some advantages to it. For example, it allows closures to refer to each other without the need for forward declarations:
const flip = n => {
if (n > 0) {
console.log("flip");
return flop(n - 1);
}
};
const flop = n => {
if (n > 0) {
console.log("flop");
return flip(n - 1);
}
};
flip(5);
But it does mean that capturing a previous binding in the initializing expression of a shadowing binding, like let x = 42; { const x = x; }, is impossible.
To access specifically a global binding, you could explicitly refer to a property of a global object, through globalThis, window or such:
var x = 20; // global scope
function f() {
let x = globalThis.x || 30;
return x;
}
console.log(f()); // 20
x = void 0;
console.log(f()); // 30
However, for shadowing outer bindings in general, you will have to resort to an awkward contraption:
{
let x = 20; // outer, but not global scope
console.log(globalThis.x); // undefined (see?)
function f() {
let outerX = x;
{
let x = outerX || 30;
return x;
}
}
console.log(f()); // 20
x = void 0;
console.log(f()); // 30
}
xis shadowing the global one, but as you are assigning a value based on an existingx, the compliler cannot find one as one is shadowed and other one is not still declared.