Things I wish I knew about… JavaScript modules

Title on background image

I have to confess it took me a while to get my head around JavaScript modules. This is at least partly because there are two different popular module systems, CommonJS and ECMAScript 2015 (usually shortened to ES2015).

The ECMAScript 2015 standard is newer than CommonJS. Personally I find it more straightforward, and I use it in preference where I can. Quite a lot of the modules on NPM still use the old style, however.

One important note - browsers only support ES2015, not CommonJS.

CommonJS

CommonJS was started in 2009 by a member of the Mozilla team, as an attempt to unify JavaScript functionality. CommonJS covers more than just modules - check the website.

Exports

In CommonJS modules, symbols are exported in an object (understandably named exports). Each property of the object is an exported symbol.

The usual object assignment rules apply. This means you can easily export a symbol with a different name than it has internal to the file.

function sayHello(name) {
  console.log("Hello", name);
}

module.exports = {
  sayHello,
  anotherSayHello: sayHello
};

Require

You can then require either the whole module or some symbols from that module.

const testModule = require("./module_name");
const { sayHello } = require("./module_name");

testModule.sayHello("world");
sayHello("second world");

NB: importing named symbols from a CJS-style module was only introduced in Node 14, and doesn't work perfectly - it relies on some heuristics within Node.

ES2015

Export

Exporting is easier in ES2015; an item (function, constant, variable etc) can be exported simply by adding the export keyword.

export function sayHello(name) {
  console.log("Hello", name);
}

export const myName = "Rumpelstiltskin";

Alternatively, you can export the items en-masse at the end of the file.

export { sayHello, myName };

Import

Like many other languages - Java, Python - you then import the parts of the module that you want to use.

import { sayHello } from "./moduleName";

sayHello("world");

Or if you want to keep them namespaced:

import * as moduleStuff from "./moduleName";

moduleStuff.sayHello(moduleStuff.myName);


Default exports

In most of the examples above, we've imported a specific named symbol from module:

import { sayHello } from "./module_name";

A module can also have a default export, which is what you then get if you don't specify what you want to import from the module.

CommonJS

CJS modules actually only export one single item - in that sense, they only have a default export. It's why the items we want to export need to be properties of an object.

ES2015

With this system you do get more flexibility. More than one thing can be exported, and you're able to mark which one you want to be the default.

export default const theAnswer = 42;

User beware

This isn't always a good thing; you're never totally sure what you're importing, and it can be imported with a completely different (and misleading) name to the original.

For example, take the trivial module basicMaths:

export default sum(x, y) { return x + y; }

That default export can be imported as anything.

import divide from "./basicMaths";

// This will not do what you expect.
divide(2, 4);

The advantage of named exports is that you import exactly what you want, no surprises.

import { divide } from "./basicMaths";

// divide is empty, so this crashes (or doesn't build, depending on how
// good your tools are). Either way it's better than using the wrong 
// function.
divide(2, 4);

Other reading

Credits

https://www.deviantart.com/laura-c-f/art/Confused-Dog-286725830

Subscribe to Paul Walker

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe