The best definition I've ever seen about ES modules is: "modules allow us to import and export stuff". Exactly! We use modules for import/export components, classes, help methods, variables and other stuff. They help us to organize our code.
In fact Module is one of the popular design pattern that allows encapsulate the code. Let's look at the implementation of this pattern.
const Module = (function () {
let _privateVariable;
let publicVariable;
function _privateMethod () {}
function publicMethod () {}
return {
publicVariable,
publicMethod,
};
})();
We have there an anonymous closure
that creates an enclosed scope
with private
and public
methods / variables and a singleton object
that get us an access to the public properties of the module.
Now let’s look at ES modules. Imagine, we have some modules...
// module A
console.log(‘a’)
// module B
console.log(‘b’)
// module C
console.log(‘c’)
Modules first!
When we import these modules into a file and execute it, the modules will be invoked
first.
import * as a from ‘./a.js’
import * as b from ‘./b.js’
import * as c from ‘./c.js’
console.log(‘index’);
Output:
a
b
c
index
Modules are evaluated only once!
The module will be evaluated only once and it does not matter how many files are module dependent.
// module A
import * as c from ‘./c.js’
console.log(‘a’)
// module B
import * as c from ‘./c.js’
console.log(‘b’)
import * as a from ‘./a.js’
import * as b from ‘./b.js’
console.log(index);
Output:
c
a
b
index
It works thanks to Module Map
. When a module is imported, a module record
is created and placed in the Module Map. When another module try to import this module, the module loader will look up in the Module Map first. Thus we can have a single
instance of each module.
Another demonstration of this idea.
// module A
import * as b from ‘./b.js’
console.log(‘a’)
// module B
import * as a from ‘./a.js’
console.log(‘b’)
import * as a from ‘./a.js’
import * as b from ‘./b.js’
console.log(index);
Output:
b
a
index
The module loading process takes several steps:
-
Parsing
- if there are any errors in your module, you will know about it first -
Loading
- if there are any imports in your module, they will be recursively imported (and themodule graph
will be built). -
Linking
- creating a module scope -
Run time
- running a module body
So, let’s look at our previous example step by step.
-> import * as a from './a.js'
|-> creating a module record for file './a.js' into the Module map
|-> parsing
|-> loading - import * as b from './b.js'
|-> creating a module record for file './b.js' into the Module map
|-> parsing
|-> loading -> import * as a from './a.js'
|-> a module record for file './a.js' already exist in the Module Map
|-> linked
|-> run
|-> linked
|-> run
This case is an example of the circular module dependency
. And if we try to call some variable from the A module in the B module, we will get a Reference Error in this case.
Module properties are exported by reference!
Let’s add a public variable into the A module.
// module A
export let value = 1;
export function setValue(val) {
value = val;
}
Now let’s import the A module into the B module...
// module B
import * as a from ‘./a.js’
a.setValue(2);
...and look at the value from the C module.
// module C
import * as a from ‘./a.js’
console.log(a.value);
Output will be '2'. Pay attention to one remarkable feature of the module - we cannot directly change the value property in module B. The 'value' property is read-only
, and we get a TypeError.
Top comments (0)