loading...

ES modules

victormagarlamov profile image Victor Magarlamov ・3 min read

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:

  1. Parsing - if there are any errors in your module, you will know about it first
  2. Loading - if there are any imports in your module, they will be recursively imported (and the module graph will be built).
  3. Linking - creating a module scope
  4. 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.

Discussion

markdown guide