JavaScript for impatient programmers (beta)
Please support this book: buy it or donate
(Ad, please don’t block.)

21. Modules

The current landscape of JavaScript modules is quite diverse: ES6 brought built-in modules, but the module systems that came before them, are still around, too. Understanding the latter helps understand the former, so let’s investigate.

21.1. Before modules: scripts

Initially, browsers only had scripts – pieces of code that were executed in global scope. As an example, consider an HTML file that loads a script file via the following HTML element:

<script src="my-library.js"></script>

In the script file, we simulate a module:

var myLibrary = function () { // Open IIFE
  // Imports (via global variables)
  var importedFunc1 = otherLibrary1.importedFunc1;
  var importedFunc2 = otherLibrary2.importedFunc2;

  function internalFunc() {
    // ···
  }

  function exportedFunc() {
    importedFunc1();
    importedFunc2();
    internalFunc();
    // ···
  }

  // Exports (assigned to global variable `myLibrary`)
  return {
    exportedFunc: exportedFunc,
  };
}(); // Close IIFE

Before we get to real modules (which were introduced with ES6), all code is written in ES5 (which didn’t have const and let, only var).

myModule is a global variable. The code that defines the module is wrapped in an immediately-invoked function expression (IIFE, coined by Ben Alman). Creating a function and calling it right away, only has one benefit compared to executing the code directly (without wrapping it): All variables defined inside the IIFE, remain local to its scope and don’t become global. At the end, we pick what we want to export and return it via an object literal. This pattern is called the revealing module pattern (coined by Christian Heilmann).

This way of simulating modules has several problems:

21.2. Module systems created prior to ES6

Prior to ECMAScript 6, JavaScript did not have built-in modules. Therefore, the flexible syntax of the language was used to implement custom module systems within the language. Two popular ones are CommonJS (targeted at the server side) and AMD (Asynchronous Module Definition, targeted at the client side).

21.2.1. Server side: CommonJS modules

The original CommonJS standard for modules was mainly created for server and desktop platforms. It was the foundation of the module system of Node.js where it achieved incredible popularity. Contributing to that popularity were Node’s package manager, npm, and tools that enabled using Node modules on the client side (browserify and webpack).

From now on, I use the terms CommonJS module and Node.js module interchangeably, even though Node.js has a few additional features. The following is an example of a Node.js module.

// Imports
var importedFunc1 = require('other-module1').importedFunc1;
var importedFunc2 = require('other-module2').importedFunc2;

function internalFunc() {
  // ···
}

function exportedFunc() {
  importedFunc1();
  importedFunc2();
  internalFunc();
  // ···
}

// Exports
module.exports = {
  exportedFunc: exportedFunc,
};

CommonJS can be characterized as follows:

21.2.2. Client side: AMD (Asynchronous Module Definition) modules

The AMD module format was created to be easier to use in browsers than the CommonJS format. Its most popular implementation is RequireJS. The following is an example of a RequireJS module.

define(['other-module1', 'other-module2'],
  function (otherModule1, otherModule2) {
    var importedFunc1 = otherModule1.importedFunc1;
    var importedFunc2 = otherModule2.importedFunc2;

    function internalFunc() {
      // ···
    }

    function exportedFunc() {
      importedFunc1();
      importedFunc2();
      internalFunc();
      // ···
    }
    return {
      exportedFunc: exportedFunc,
    };
  });

AMD can be characterized as follows:

21.2.3. Characteristics of JavaScript modules

Looking at CommonJS and AMD, similarities between JavaScript module systems emerge:

21.3. ECMAScript modules

ECMAScript modules were introduced with ES6: They stand firmly in the tradition of JavaScript modules and share many of the characteristics of existing module systems:

ES modules also have new benefits:

This is an example of ES module syntax:

import {importedFunc1} from 'other-module1';
import {importedFunc2} from 'other-module2';

function internalFunc() {
  ···
}

export function exportedFunc() {
  importedFunc1();
  importedFunc2();
  internalFunc();
  ···
}

From now on, “module” means “ECMAScript module”.

21.3.1. ECMAScript modules: three parts

ECMAScript modules comprise three parts:

  1. Declarative module syntax: What is a module? How are imports and exports declared?
  2. The semantics of the syntax: How are the variable bindings handled that are created by imports? How are exported variable bindings handled?
  3. A programmatic loader API for configuring module loading.

Parts 1 and 2 were introduced with ES6. Work on Part 3 is ongoing.

21.4. Named exports

Each module can have zero or more named exports.

As an example, consider the following three files:

lib/my-math.js
main1.js
main2.js

Module my-math.js has two named exports: square and MY_CONSTANT.

let notExported = 'abc';
export function square(x) {
  return x * x;
}
export const MY_CONSTANT = 123;

Module main1.js has a single named import, square:

import {square} from './lib/my-math.js';
assert.equal(square(3), 9);

Module main2.js has a so-called namespace import – all named exports of my-math.js can be accessed as properties of the object myMath:

import * as myMath from './lib/my-math.js';
assert.equal(myMath.square(3), 9);

  Exercise: Named exports

exercises/modules/export_named_test.js

21.5. Default exports

Each module can have at most one default export. The idea is that the module is the default-exported value. A module can have both named exports and a default export, but it’s usually better to stick to one export style per module.

As an example for default exports, consider the following two files:

my-func.js
main.js

Module my-func.js has a default export:

export default function () {
  return 'Hello!';
}

Module main.js default-imports the exported function:

import myFunc from './my-func.js';
assert.equal(myFunc(), 'Hello!');

Note the syntactic difference: The curly braces around named imports indicate that we are reaching into the module, while a default import is the module.

The most common use case for a default export is a module that contains a single function or a single class.

21.5.1. The two styles of default-exporting

There are two styles of doing default exports.

First, you can label existing declarations with export default:

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

Second, you can directly default-export values. In that style, export default is itself much like a declaration.

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

Why are there two default export styles? The reason is that export default can’t be used to label const: const may define multiple values, but export default needs exactly one value.

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

With this hypothetical code, you don’t know which one of the three values is the default export.

  Exercise: Default exports

exercises/modules/export_default_test.js

21.6. Naming modules

There are no established best practices for naming module files and the variables they are imported into.

In this chapter, I’ve used the following naming style:

What are the rationales behind this style?

I also like underscore-cased module file names, because you can directly use these names for namespace imports (without any translation):

import * as my_module from './my_module.js';

But that style does not work for default imports: I like underscore-casing for namespace objects, but it is not a good choice for functions etc.

21.7. Imports are read-only views on exports

So far, we have used imports and exports intuitively and everything seems to have worked as expected. But now it is time to take a closer look at how imports and exports are really related.

Consider the following two modules:

counter.js
main.js

counter.js exports a (mutable!) variable and a function:

export let counter = 3;
export function incCounter() {
  counter++;
}

main.js name-imports both exports. When we use incCounter(), we discover that the connection to counter is live – we can always access the live state of that variable:

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

// The imported value `counter` is live
assert.equal(counter, 3);
incCounter();
assert.equal(counter, 4);

Note that, while the connection is live and we can read counter, we cannot change this variable (e.g. via counter++).

Why do ES modules behave this way?

First, it is easier to split modules, because previously shared variables can become exports and imports.

Second, this behavior is crucial for cyclic imports. The exports of a module are known before executing it. Therefore, if a module L and a module M import each other, cyclically, the following steps happen:

Cyclic imports are something that you should avoid as much as possible, but they can arise in complex systems or when refactoring systems. It is important that things don’t break when that happens.

21.8. Module specifiers

One key rule is:

All ES modules specifiers must be valid URLs.

That means that paths to sibling files must include file name extensions. Beyond that, everything is still in flux. CommonJS distinguishes several kinds of module specifiers:

In Node.js, module specifiers are handled as follows:

Browsers handle module specifiers as follows:

Note that bundling tools such as browserify and webpack that compile multiple modules into single files are less restrictive w.r.t. module specifiers than browsers, because they operate at compile time, not at runtime.

21.9. Syntactic pitfall: importing is not destructuring

Both importing and destructuring look similar:

import {foo} from './bar.js'; // import
const {foo} = require('./bar.js'); // destructuring

But they are quite different:

21.10. Preview: loading modules dynamically

So far, the only way to import a module has been via an import statement. And with those statements, the module specifier is always fixed. That is, you can’t change what you import depending on a condition, you can’t assemble a specifier from parts and you can’t read it from a file.

An upcoming JavaScript feature changes that: The import() operator is used like an asynchronous function.

Consider the following files:

lib/my-math.js
main1.js
main2.js

We have already seen module my-math.js:

let notExported = 'abc';
export function square(x) {
  return x * x;
}
export const MY_CONSTANT = 123;

This is what using import() looks like:

const dir = './lib/';
const moduleSpecifier = dir + 'my-math.js';

function loadConstant() {
  return import(moduleSpecifier)
  .then(myMath => {
    const result = myMath.MY_CONSTANT;
    assert.equal(result, 123);
    return result;
  });
}

Method .then() is part of Promises, a mechanism for handling asynchronous results, which is covered later in this book.

Two things in this code weren’t possible before:

Next, we’ll implement the exact same functionality, but via a so-called async function, which provides nicer syntax for Promises.

const dir = './lib/';
const moduleSpecifier = dir + 'my-math.js';

async function loadConstant() {
  const myMath = await import(moduleSpecifier);
  const result = myMath.MY_CONSTANT;
  assert.equal(result, 123);
  return result;
}

Alas, import() isn’t part of JavaScript, but probably will be, relatively soon. That means that support is mixed and may be inconsistent.

21.11. Further reading

  Quiz

See quiz app.