Understanding ES6 modules. In-depth article.

Markian 0x
4 min readJun 14, 2017

Intro

As this is an advanced blog post I am not going to describe the purpose of ESM — you probably know it not worse than I do.
If you are completely new to ES6 modules I would recommend reading this article from Mozilla blog.
But the syntax is pretty straightforward — if we want to import something we use import keyword. Export keyword is used for exporting something from module outside.
That all is clear and understandable. And this understanding of things will work…. until you write a simple application without some tricks… cyclic dependency for example.

That was exactly what happened to me. And I started digging.
To really understand something you have to understand level deeper. In this case, you have to understand how the ES6 modules work under cover.

What really happens under cover?

The process that is defined in the ESM spec is like this:

  1. Loading
  2. Parsing
  3. Evaluation

Loading.

The implementation loads the module into the memory.

Parsing.

The implementation reads the source code of the module and checks for syntax errors(that flow is common to what we may know from usual JS, but what happens next may be interesting though).
The implementation also forms so-called Module Record that represents the shape of the module.

A Module Record encapsulates structural information about the imports and exports of a single module. This information is used to link the imports and exports of sets of connected modules.

So basically part of the Module Record is a static listing of the symbols, that are pointers to things that do not exist yet (will exist after evaluation happens).

Why should you have identifiers that point to things that don’t yet exist?
If you have for example a module that has an import statement like this import { foo } from 'bar' ESM implementation can verify whether module bar actually exports foo during parsing.

In the context of forming the list of exports, the process is actually very similar to forming(during parsing time also) a Lexical Environment object or hoisting. It is important to note the distinction between variables and functions — while first ones hoist with an undefined as value, the second ones hoist as a holistic structure. The same happens for modules(in the global context only). Let’s say — if we have exported function we can import it in any module and it will be ready for use even if the module where it is declared isn’t evaluated yet. I will show where this can be useful later.

It’s also worth mentioning that all import statements are processed during the parsing. That means that every time ESM implementation encounters import statement, the new loading process is initiated and that happens recursively until all files are loaded.

Evaluation.

Finally, the implementation runs the statements in the body of each newly-loaded module. By this time, import processing is already finished, so when execution reaches a line of code where there’s an import declaration… nothing happens!

Example

Now imagine you have 2 modules — App, A & B.

App:

import A from 'A'

A:

import { baz } from 'B'
console.log(baz)
export const foo = 2

B:

import { foo } from 'A'
console.log(foo)
export const baz = 1

What do you think will the output in the console be? Pretty interesting, right?
Let’s walk from the beginning.

  1. Loading App. Parsing App. Forming App Module Record. Loading A(1st line).
  2. Parsing A. Forming A Module Record. Loading B(1st line).
  3. Parsing B. Forming B Module Record. Taking A’s Module Record from cache(1st line).
  4. Evaluation starts. The process can be imagined like this App(A(B()))
  5. Evaluating B. // undefined in console
  6. Evaluating A. // 1 in console
  7. Evaluating App

As stated above you can export a function and it will be available anywhere regardless of what is evaluation state of the module. So in our example, if you change the 3rd line of A module to something like this — export function foo(){}, the first console won’t make us sad with undefined.

I encourage you to try this example(or different ones) yourself. For now, you can play with Babel … reviewing the compiled file and understanding the way Babel simulates ESM spec can help a lot.

Thanksgiving

I would like to express a huge thank to the James M Snell for these excellent articles about distinctions between Node RequireJS mechanism and ESM spec. Also the article ES6 In Depth: Modules of Jason Orendorff was very helpful.

Thanks for reading.

#tech #javascript #es6

--

--