ESNext — Modules

This is the third post in our on-going series Introducing ESNext. So far, we've talked about the Map and Set data structures. Now, let's dig into something more complex: dependency management.

In the beginning, there was the single thousand line of code file of Javascript... and it was not good.

Then, we "modularized" out code by splitting them into separate files and included them via script tags in precisely the correct order in our HTML.

Eventually, we built tools to take this list and concatenate them back into that single file for performance reasons.

As projects grew and we began to depend on more and more third-party libraries, this approach became very cumbersome. Adventurous front-end developers played around with RequireJS, while backend developers used CommonJS (via Node). This allowed modularity, prevented global variables from accidentally being created and let each module expose it's own public API.

But, why have two different systems, when the problem can be solved once and for all at the language level. ESNext Modules solve dependencies in Javascript once and for all.


Each module is represented by a single Javascript file. Importing a file will reference the actual path (unlike some old module systems like Google Closure Compiler which divorced the module name from its disk location).

Inside each module file, methods and variables that external modules can consume must be exported. This is accomplished in several ways.

Any method or variable can be added to the named list of exports by prefixing the definition with export:

	export function useMe {}
	export const SOME_CONSTANT = 1;
	export const ANOTHER_CONSTANT = 2;

This works great for a utility module which exports a bunch of smaller methods. However, sometimes your module represents a single object, class or method. In this case, you can export that single item as the "default" value for the module:

	export default class MyClass

Anything not exported by a module will be inaccessible outside that module's file.


The import command allows modules to depend on each other. As above, there are several ways to import methods and even optionally rename them in the scope of the new module.

In the example of the default export, that can be pulled in like so:

	import MyLocalClass from './my_class';

Notice, that I can rename the imported class to MyLocalClass.

Given the example with multiple exports, a module can import these like so:

	import * as util from './util';

This will create a local variable named util with the keys useMe, SOME_CONSTANT and ANOTHER_CONSTANT. Alternatively, you may only want a subset of the exports. These can be pulled out with destructuring:

	import { SOME_CONSTANT, ANOTHER_CONSTANT } from './util';

Now, you'll have two constants in scope, but no access to useMe. This form also allows renaming:


Which would keep the ANOTHER_CONSTANT in the local scope, but now, you'd access DIFFERENT_CONSTANT for the other variable.


Up until now, we've used relative paths to files for dependencies, but most third-party libraries will be hosted in NPM instead. Given a system like Babel.js or Browserify, these can be imported with non-relative paths. For example:

	import $ from 'jquery';
	import _ from 'underscore';

Having a native module system allows Javascript applications to be split into smaller modules, avoids naming collisions and global namespace pollution while also generating a dependency graph. Having this graph makes is very easy to break up your compiled javascript into smaller chunks. For example, you could compile all the shared code in your app to shared.js while splitting out separate per-page files for the page-specific Javascript. Why include all of D3.js for all your users, when it's only used on one single dashboard page.

At Instrument, we've been using modules for years via the Google Closure Compiler. We are incredibly excited to have official support for modularity in ESNext and look forward to the rest of the front-end community discovering its usefulness.