Ecmascript 6: Modules

[Fuente: http://exploringjs.com/es6/ch_modules.html]

16.1 Overview

JavaScript has had modules for a long time. However, they were implemented via libraries, not built into the language. ES6 is the first time that JavaScript has built-in modules.

ES6 modules are stored in files. There is exactly one module per file and one file per module. You have two ways of exporting things from a module. These two ways can be mixed, but it is usually better to use them separately.

16.1.1 Multiple named exports

There can be multiple named exports:

//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}

//------ main.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

You can also import the complete module:

//------ main.js ------
import * as lib from 'lib';
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5

16.1.2 Single default export

There can be a single default export. For example, a function:

//------ myFunc.js ------
export default function () { ··· } // no semicolon!

//------ main1.js ------
import myFunc from 'myFunc';
myFunc();

Or a class:

//------ MyClass.js ------
export default class { ··· } // no semicolon!

//------ main2.js ------
import MyClass from 'MyClass';
const inst = new MyClass();

Note that there is no semicolon at the end if you default-export a function or a class (which are anonymous declarations).

16.1.3 Browsers: scripts versus modules

Scripts Modules
HTML element <script> <script type="module">
Default mode non-strict strict
Top-level variables are global local to module
Value of thisat top level window undefined
Executed synchronously asynchronously
Declarative imports (importstatement) no yes
Programmatic imports (Promise-based API) yes yes
File extension .js .js

 

16.2 Modules in JavaScript

Even though JavaScript never had built-in modules, the community has converged on a simple style of modules, which is supported by libraries in ES5 and earlier. This style has also been adopted by ES6:

  • Each module is a piece of code that is executed once it is loaded.
  • In that code, there may be declarations (variable declarations, function declarations, etc.).
    • By default, these declarations stay local to the module.
    • You can mark some of them as exports, then other modules can import them.
  • A module can import things from other modules. It refers to those modules via module specifiers, strings that are either:
    • Relative paths ('../model/user'): these paths are interpreted relatively to the location of the importing module. The file extension .js can usually be omitted.
    • Absolute paths ('/lib/js/helpers'): point directly to the file of the module to be imported.
    • Names ('util'): What modules names refer to has to be configured.
  • Modules are singletons. Even if a module is imported multiple times, only a single “instance” of it exists.

This approach to modules avoids global variables, the only things that are global are module specifiers.

16.2.1 ECMAScript 5 module systems

It is impressive how well ES5 module systems work without explicit support from the language. The two most important (and unfortunately incompatible) standards are:

  • CommonJS Modules: The dominant implementation of this standard is in Node.js (Node.js modules have a few features that go beyond CommonJS). Characteristics:
    • Compact syntax
    • Designed for synchronous loading and servers
  • Asynchronous Module Definition (AMD): The most popular implementation of this standard is RequireJS. Characteristics:
    • Slightly more complicated syntax, enabling AMD to work without eval() (or a compilation step)
    • Designed for asynchronous loading and browsers

The above is but a simplified explanation of ES5 modules. If you want more in-depth material, take a look at “Writing Modular JavaScript With AMD, CommonJS & ES Harmony” by Addy Osmani.

16.2.2 ECMAScript 6 modules

The goal for ECMAScript 6 modules was to create a format that both users of CommonJS and of AMD are happy with:

  • Similarly to CommonJS, they have a compact syntax, a preference for single exports and support for cyclic dependencies.
  • Similarly to AMD, they have direct support for asynchronous loading and configurable module loading.

Being built into the language allows ES6 modules to go beyond CommonJS and AMD (details are explained later):

  • Their syntax is even more compact than CommonJS’s.
  • Their structure can be statically analyzed (for static checking, optimization, etc.).
  • Their support for cyclic dependencies is better than CommonJS’s.

The ES6 module standard has two parts:

  • Declarative syntax (for importing and exporting)
  • Programmatic loader API: to configure how modules are loaded and to conditionally load modules

16.3 The basics of ES6 modules

There are two kinds of exports: named exports (several per module) and default exports (one per module). As explained later, it is possible use both at the same time, but usually best to keep them separate.

16.3.1 Named exports (several per module)

A module can export multiple things by prefixing its declarations with the keyword export. These exports are distinguished by their names and are called named exports.

//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}

//------ main.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

There are other ways to specify named exports (which are explained later), but I find this one quite convenient: simply write your code as if there were no outside world, then label everything that you want to export with a keyword.

If you want to, you can also import the whole module and refer to its named exports via property notation:

//------ main.js ------
import * as lib from 'lib';
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5

The same code in CommonJS syntax: For a while, I tried several clever strategies to be less redundant with my module exports in Node.js. Now I prefer the following simple but slightly verbose style that is reminiscent of the revealing module pattern:

//------ lib.js ------
var sqrt = Math.sqrt;
function square(x) {
    return x * x;
}
function diag(x, y) {
    return sqrt(square(x) + square(y));
}
module.exports = {
    sqrt: sqrt,
    square: square,
    diag: diag,
};

//------ main.js ------
var square = require('lib').square;
var diag = require('lib').diag;
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

16.3.2 Default exports (one per module)

Modules that only export single values are very popular in the Node.js community. But they are also common in frontend development where you often have classes for models and components, with one class per module. An ES6 module can pick a default export, the main exported value. Default exports are especially easy to import.

The following ECMAScript 6 module “is” a single function:

//------ myFunc.js ------
export default function () {} // no semicolon!

//------ main1.js ------
import myFunc from 'myFunc';
myFunc();

An ECMAScript 6 module whose default export is a class looks as follows:

//------ MyClass.js ------
export default class {} // no semicolon!

//------ main2.js ------
import MyClass from 'MyClass';
const inst = new MyClass();

There are two styles of default exports:

  1. Labeling declarations
  2. Default-exporting values directly
16.3.2.1 Default export style 1: labeling declarations

You can prefix any function declaration (or generator function declaration) or class declaration with the keywords export default to make it the default export:

export default function foo() {} // no semicolon!
export default class Bar {} // no semicolon!

You can also omit the name in this case. That makes default exports the only place where JavaScript has anonymous function declarations and anonymous class declarations:

export default function () {} // no semicolon!
export default class {} // no semicolon!
16.3.2.1.1 Why anonymous function declarations and not anonymous function expressions?

When you look at the previous two lines of code, you’d expect the operands of export default to be expressions. They are only declarations for reasons of consistency: operands can be named declarations, interpreting their anonymous versions as expressions would be confusing (even more so than introducing new kinds of declarations).

If you want the operands to be interpreted as expressions, you need to use parentheses:

export default (function () {});
export default (class {});
16.3.2.2 Default export style 2: default-exporting values directly

The values are produced via expressions:

export default 'abc';
export default foo();
export default /^xyz$/;
export default 5 * 7;
export default { no: false, yes: true };

Each of these default exports has the following structure.

export default «expression»;

That is equivalent to:

const __default__ = «expression»;
export { __default__ as default }; // (A)

The statement in line A is an export clause (which is explained in a later section).

16.3.2.2.1 Why two default export styles?

The second default export style was introduced because variable declarations can’t be meaningfully turned into default exports if they declare multiple variables:

export default const foo = 1, bar = 2, baz = 3; // not legal JavaScript!

Which one of the three variables foo, bar and baz would be the default export?

16.3.3 Imports and exports must be at the top level

As explained in more detail later, the structure of ES6 modules is static, you can’t conditionally import or export things. That brings a variety of benefits.

This restriction is enforced syntactically by only allowing imports and exports at the top level of a module:

if (Math.random()) {
    import 'foo'; // SyntaxError
}

// You can’t even nest `import` and `export`
// inside a simple block:
{
    import 'foo'; // SyntaxError
}

16.3.4 Imports are hoisted

Module imports are hoisted (internally moved to the beginning of the current scope). Therefore, it doesn’t matter where you mention them in a module and the following code works without any problems:

foo();

import { foo } from 'my_module';

16.3.5 Imports are read-only views on exports

The imports of an ES6 module are read-only views on the exported entities. That means that the connections to variables declared inside module bodies remain live, as demonstrated in the following code.

//------ lib.js ------
export let counter = 3;
export function incCounter() {
    counter++;
}

//------ main.js ------
import { counter, incCounter } from './lib';

// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4

How that works under the hood is explained in a later section.

Imports as views have the following advantages:

  • They enable cyclic dependencies, even for unqualified imports (as explained in the next section).
  • Qualified and unqualified imports work the same way (they are both indirections).
  • You can split code into multiple modules and it will continue to work (as long as you don’t try to change the values of imports).

16.3.6 Support for cyclic dependencies

Two modules A and B are cyclically dependent on each other if both A (possibly indirectly/transitively) imports B and B imports A. If possible, cyclic dependencies should be avoided, they lead to A and B being tightly coupled – they can only be used and evolved together.

Why support cyclic dependencies, then? Occasionally, you can’t get around them, which is why support for them is an important feature. A later section has more information.

Let’s see how CommonJS and ECMAScript 6 handle cyclic dependencies.

16.3.6.1 Cyclic dependencies in CommonJS

The following CommonJS code correctly handles two modules a and b cyclically depending on each other.

//------ a.js ------
var b = require('b');
function foo() {
    b.bar();
}
exports.foo = foo;

//------ b.js ------
var a = require('a'); // (i)
function bar() {
    if (Math.random()) {
        a.foo(); // (ii)
    }
}
exports.bar = bar;

If module a is imported first then, in line i, module b gets a’s exports object before the exports are added to it. Therefore, b cannot access a.foo in its top level, but that property exists once the execution of a is finished. If bar() is called afterwards then the method call in line ii works.

As a general rule, keep in mind that with cyclic dependencies, you can’t access imports in the body of the module. That is inherent to the phenomenon and doesn’t change with ECMAScript 6 modules.

The limitations of the CommonJS approach are:

  • Node.js-style single-value exports don’t work. There, you export single values instead of objects:
      module.exports = function () { ··· };
    

    If module a did that then module b’s variable a would not be updated once the assignment is made. It would continue to refer to the original exports object.

  • You can’t use named exports directly. That is, module b can’t import foo like this:
      var foo = require('a').foo;
    

    foo would simply be undefined. In other words, you have no choice but to refer to foo via a.foo.

These limitations mean that both exporter and importers must be aware of cyclic dependencies and support them explicitly.

16.3.6.2 Cyclic dependencies in ECMAScript 6

ES6 modules support cyclic dependencies automatically. That is, they do not have the two limitations of CommonJS modules that were mentioned in the previous section: default exports work, as do unqualified named imports (lines i and iii in the following example). Therefore, you can implement modules that cyclically depend on each other as follows.

//------ a.js ------
import {bar} from 'b'; // (i)
export function foo() {
    bar(); // (ii)
}

//------ b.js ------
import {foo} from 'a'; // (iii)
export function bar() {
    if (Math.random()) {
        foo(); // (iv)
    }
}

This code works, because, as explained in the previous section, imports are views on exports. That means that even unqualified imports (such as bar in line ii and fooin line iv) are indirections that refer to the original data. Thus, in the face of cyclic dependencies, it doesn’t matter whether you access a named export via an unqualified import or via its module: There is an indirection involved in either case and it always works.