To explain what's happening internally, when b()
is called a new environment record (ER) is created. Conceptually, the "ER" represents a "scope" and is responsible for holding the bindings (variables) which are created within the b
function. The ER can have a binding created within it but no value associated with it, so a binding can be instantiated but not initialised to a value (due to this, the binding is in the TDZ). In the case of b
, an unitialised binding for x
is created within b
's ER before any of the code within b
has actually run. It is only once the code within b()
starts running and we reach the line const x = 10;
does the x
binding become initialized with a value. That means that even though you're calling a()
before the line const x = 10;
is evaluated, the x
binding has already been created, just without a value (this is why x
is "hoisted")
When you try and access x
from within a
, a
's scope (ie: its ER) is first checked for x
. As x
isn't defined within a
, we walk up the scope chain to the surrounding scope to search that for x
. As the surrounding scope is b
's scope, b
's ER is checked for the x
binding, which as explained above, it has. That's why we stop searching the scope chain when we check b
's scope, as we've already found a binding for x
, and thus we never check the global scope for its x
binding. However, as the binding for x
that we found within b
's ER has no value and is uninitialised, JavaScript will throw a ReferenceError.