Reputation: 2201
// a.js
import * as bModule from './b.js'
export const b = bModule
// b.js
import * as aModule from './a.js'
export const a = aModule
If a.js
is an entry point of our app then b.js
would run firstly. Calling of console.log(aModule)
from b.js
at this stage results in such output:
[Module] {
b: <uninitialized>,
}
And if b.js
try to access aModule.b
it will end up with the ReferenceError: b is not defined
.
ReferenceError
is thrown when a code uses an identifier (variable) hasn't declared yet. However, in this case aModule
does have the property b
(the fact is borne out by console.log
). Moreover, when we access an uninitialized object property we just get the undefined
value but not the exception.
So how this behavior can be understood? Is it specified?
Upvotes: 1
Views: 57
Reputation: 1075209
ReferenceError is thrown when a code uses an identifier (variable) hasn't declared yet.
Not only then. :-)
Moreover, when we access an uninitialized object property we just get the undefined value but not the exception.
Yes, but modules are modern constructs, and you're using those another modern construct: let
. It's defined to fail if used before it could be initialized, rather than provide ambiguous values.
Yes, it's specified behavior. Modules go through a series of phases, and during that process their exports of let
, const
, or class
bindings are created as uninitialized bindings, and then later those exports are initialized with values. When modules have cycles (circular dependencies), it's possible to see an export before it's initialized, causing a ReferenceError
.
It's the same Temporal Dead Zone (TDZ) we can see here:
let a = 21;
console.log(a); // 21
if (true) {
console.log(a); // ReferenceError
let a = 42;
}
The declaration for a
within the if
block declares it throughout the block, but you can't access it until the declaration is reached in the step-by-step execution of the code. You can see that the declaration takes effect throughout the block by the fact that we can't access the outer a
from inside the block.
The same TDZ applies to exports that haven't been initialized yet.
You'll only have this problem with top-level code in a module, since the module graph will be fully resolved before any callbacks occur. Your code in b.js
would be safe using a.b
in a function called in response to an event (or called from a.js
).
If you needed to use a.b
in b.js
at the top level, you'd need to run the call after b.js
's top-level code execution was complete, which you can do via setTimeout
or similar:
setTimeout(() => {
console.log(a.b); // Shows the `b` module namespace object's contents
}, 0);
Upvotes: 2
Reputation: 29314
This behavior is because of the temporal dead zone
.
Unlike identifiers declared using var
, identifiers declared using let
or const
are marked as "not yet initialized" and they can't be accessed until their declaration has actually executed during step-by-step execution of the javascript code.
Since b
in a.js
is defined with const
, it is hoisted but you can't access it before its declaration has actually executed. Same would be the case if b
was declared with let
.
If you declare b
using var
, then you would see undefined
as the output.
Upvotes: 3