Modules
I don’t think it’s an exaggeration to suggest that the single most important code organization pattern in all of JavaScript is, and always has been, the module. For myself, and I think for a large cross-section of the community, the module pattern drives the vast majority of code.
The Old Way
The traditional module pattern is based on an outer function with inner variables and functions, and a returned “public API” with methods that have closure over the inner data and capabilities. It’s often expressed like this:
function Hello(name) {
function greeting() {
console.log( "Hello " + name + "!" );
}
// public API
return {
greeting: greeting
};
}
var me = Hello( "Kyle" );
me.greeting(); // Hello Kyle!
This Hello(..)
module can produce multiple instances by being called subsequent times. Sometimes, a module is only called for as a singleton (i.e., it just needs one instance), in which case a slight variation on the previous snippet, using an IIFE, is common:
var me = (function Hello(name){
function greeting() {
console.log( "Hello " + name + "!" );
}
// public API
return {
greeting: greeting
};
})( "Kyle" );
me.greeting(); // Hello Kyle!
This pattern is tried and tested. It’s also flexible enough to have a wide assortment of variations for a number of different scenarios.
One of the most common is the Asynchronous Module Definition (AMD), and another is the Universal Module Definition (UMD). We won’t cover the particulars of these patterns and techniques here, but they’re explained extensively in many places online.
Moving Forward
As of ES6, we no longer need to rely on the enclosing function and closure to provide us with module support. ES6 modules have first class syntactic and functional support.
Before we get into the specific syntax, it’s important to understand some fairly significant conceptual differences with ES6 modules compared to how you may have dealt with modules in the past:
ES6 uses file-based modules, meaning one module per file. At this time, there is no standardized way of combining multiple modules into a single file.
That means that if you are going to load ES6 modules directly into a browser web application, you will be loading them individually, not as a large bundle in a single file as has been common in performance optimization efforts.
It’s expected that the contemporaneous advent of HTTP/2 will significantly mitigate any such performance concerns, as it operates on a persistent socket connection and thus can very efficiently load many smaller files in parallel and interleaved with one another.
The API of an ES6 module is static. That is, you define statically what all the top-level exports are on your module’s public API, and those cannot be amended later.
Some uses are accustomed to being able to provide dynamic API definitions, where methods can be added/removed/replaced in response to runtime conditions. Either these uses will have to change to fit with ES6 static APIs, or they will have to restrain the dynamic changes to properties/methods of a second-level object.
- ES6 modules are singletons. That is, there’s only one instance of the module, which maintains its state. Every time you import that module into another module, you get a reference to the one centralized instance. If you want to be able to produce multiple module instances, your module will need to provide some sort of factory to do it.
The properties and methods you expose on a module’s public API are not just normal assignments of values or references. They are actual bindings (almost like pointers) to the identifiers in your inner module definition.
In pre-ES6 modules, if you put a property on your public API that holds a primitive value like a number or string, that property assignment was by value-copy, and any internal update of a corresponding variable would be separate and not affect the public copy on the API object.
With ES6, exporting a local private variable, even if it currently holds a primitive string/number/etc, exports a binding to the variable. If the module changes the variable’s value, the external import binding now resolves to that new value.
Importing a module is the same thing as statically requesting it to load (if it hasn’t already). If you’re in a browser, that implies a blocking load over the network. If you’re on a server (i.e., Node.js), it’s a blocking load from the filesystem.
However, don’t panic about the performance implications. Because ES6 modules have static definitions, the import requirements can be statically scanned, and loads will happen preemptively, even before you’ve used the module.
ES6 doesn’t actually specify or handle the mechanics of how these load requests work. There’s a separate notion of a Module Loader, where each hosting environment (browser, Node.js, etc.) provides a default Loader appropriate to the environment. The importing of a module uses a string value to represent where to get the module (URL, file path, etc.), but this value is opaque in your program and only meaningful to the Loader itself.
You can define your own custom Loader if you want more fine-grained control than the default Loader affords — which is basically none, as it’s totally hidden from your program’s code.
As you can see, ES6 modules will serve the overall use case of organizing code with encapsulation, controlling public APIs, and referencing dependency imports. But they have a very particular way of doing so, and that may or may not fit very closely with how you’ve already been doing modules for years.
CommonJS
There’s a similar, but not fully compatible, module syntax called CommonJS, which is familiar to those in the Node.js ecosystem.
For lack of a more tactful way to say this, in the long run, ES6 modules essentially are bound to supersede all previous formats and standards for modules, even CommonJS, as they are built on syntactic support in the language. This will, in time, inevitably win out as the superior approach, if for no other reason than ubiquity.
We face a fairly long road to get to that point, though. There are literally hundreds of thousands of CommonJS style modules in the server-side JavaScript world, and 10 times that many modules of varying format standards (UMD, AMD, ad hoc) in the browser world. It will take many years for the transitions to make any significant progress.
In the interim, module transpilers/converters will be an absolute necessity. You might as well just get used to that new reality. Whether you author in regular modules, AMD, UMD, CommonJS, or ES6, these tools will have to parse and convert to a format that is suitable for whatever environment your code will run in.
For Node.js, that probably means (for now) that the target is CommonJS. For the browser, it’s probably UMD or AMD. Expect lots of flux on this over the next few years as these tools mature and best practices emerge.
From here on out, my best advice on modules is this: whatever format you’ve been religiously attached to with strong affinity, also develop an appreciation for and understanding of ES6 modules, such as they are, and let your other module tendencies fade. They are the future of modules in JS, even if that reality is a bit of a ways off.
The New Way
The two main new keywords that enable ES6 modules are import
and export
. There’s lots of nuance to the syntax, so let’s take a deeper look.
Warning: An important detail that’s easy to overlook: both import
and export
must always appear in the top-level scope of their respective usage. For example, you cannot put either an import
or export
inside an if
conditional; they must appear outside of all blocks and functions.
export
ing API Members
The export
keyword is either put in front of a declaration, or used as an operator (of sorts) with a special list of bindings to export. Consider:
export function foo() {
// ..
}
export var awesome = 42;
var bar = [1,2,3];
export { bar };
Another way of expressing the same exports:
function foo() {
// ..
}
var awesome = 42;
var bar = [1,2,3];
export { foo, awesome, bar };
These are all called named exports, as you are in effect exporting the name bindings of the variables/functions/etc.
Anything you don’t label with export
stays private inside the scope of the module. That is, although something like var bar = ..
looks like it’s declaring at the top-level global scope, the top-level scope is actually the module itself; there is no global scope in modules.
Note: Modules do still have access to window
and all the “globals” that hang off it, just not as lexical top-level scope. However, you really should stay away from the globals in your modules if at all possible.
You can also “rename” (aka alias) a module member during named export:
function foo() { .. }
export { foo as bar };
When this module is imported, only the bar
member name is available to import; foo
stays hidden inside the module.
Module exports are not just normal assignments of values or references, as you’re accustomed to with the =
assignment operator. Actually, when you export something, you’re exporting a binding (kinda like a pointer) to that thing (variable, etc.).
Within your module, if you change the value of a variable you already exported a binding to, even if it’s already been imported (see the next section), the imported binding will resolve to the current (updated) value.
Consider:
var awesome = 42;
export { awesome };
// later
awesome = 100;
When this module is imported, regardless of whether that’s before or after the awesome = 100
setting, once that assignment has happened, the imported binding resolves to the 100
value, not 42
.
That’s because the binding is, in essence, a reference to, or a pointer to, the awesome
variable itself, rather than a copy of its value. This is a mostly unprecedented concept for JS introduced with ES6 module bindings.
Though you can clearly use export
multiple times inside a module’s definition, ES6 definitely prefers the approach that a module has a single export, which is known as a default export. In the words of some members of the TC39 committee, you’re “rewarded with simpler import
syntax” if you follow that pattern, and conversely “penalized” with more verbose syntax if you don’t.
A default export sets a particular exported binding to be the default when importing the module. The name of the binding is literally default
. As you’ll see later, when importing module bindings you can also rename them, as you commonly will with a default export.
There can only be one default
per module definition. We’ll cover import
in the next section, and you’ll see how the import
syntax is more concise if the module has a default export.
There’s a subtle nuance to default export syntax that you should pay close attention to. Compare these two snippets:
function foo(..) {
// ..
}
export default foo;
And this one:
function foo(..) {
// ..
}
export { foo as default };
In the first snippet, you are exporting a binding to the function expression value at that moment, not to the identifier foo
. In other words, export default ..
takes an expression. If you later assign foo
to a different value inside your module, the module import still reveals the function originally exported, not the new value.
By the way, the first snippet could also have been written as:
export default function foo(..) {
// ..
}
Warning: Although the function foo..
part here is technically a function expression, for the purposes of the internal scope of the module, it’s treated like a function declaration, in that the foo
name is bound in the module’s top-level scope (often called “hoisting”). The same is true for export default class Foo..
. However, while you can do export var foo = ..
, you currently cannot do export default var foo = ..
(or let
or const
), in a frustrating case of inconsistency. At the time of this writing, there’s already discussion of adding that capability in soon, post-ES6, for consistency sake.
Recall the second snippet again:
function foo(..) {
// ..
}
export { foo as default };
In this version of the module export, the default export binding is actually to the foo
identifier rather than its value, so you get the previously described binding behavior (i.e., if you later change foo
‘s value, the value seen on the import side will also be updated).
Be very careful of this subtle gotcha in default export syntax, especially if your logic calls for export values to be updated. If you never plan to update a default export’s value, export default ..
is fine. If you do plan to update the value, you must use export { .. as default }
. Either way, make sure to comment your code to explain your intent!
Because there can only be one default
per module, you may be tempted to design your module with one default export of an object with all your API methods on it, such as:
export default {
foo() { .. },
bar() { .. },
..
};
That pattern seems to map closely to how a lot of developers have already structured their pre-ES6 modules, so it seems like a natural approach. Unfortunately, it has some downsides and is officially discouraged.
In particular, the JS engine cannot statically analyze the contents of a plain object, which means it cannot do some optimizations for static import
performance. The advantage of having each member individually and explicitly exported is that the engine can do the static analysis and optimization.
If your API has more than one member already, it seems like these principles — one default export per module, and all API members as named exports — are in conflict, doesn’t it? But you can have a single default export as well as other named exports; they are not mutually exclusive.
So, instead of this (discouraged) pattern:
export default function foo() { .. }
foo.bar = function() { .. };
foo.baz = function() { .. };
You can do:
export default function foo() { .. }
export function bar() { .. }
export function baz() { .. }
Note: In this previous snippet, I used the name foo
for the function that default
labels. That foo
name, however, is ignored for the purposes of export — default
is actually the exported name. When you import this default binding, you can give it whatever name you want, as you’ll see in the next section.
Alternatively, some will prefer:
function foo() { .. }
function bar() { .. }
function baz() { .. }
export { foo as default, bar, baz, .. };
The effects of mixing default and named exports will be more clear when we cover import
shortly. But essentially it means that the most concise default import form would only retrieve the foo()
function. The user could additionally manually list bar
and baz
as named imports, if they want them.
You can probably imagine how tedious that’s going to be for consumers of your module if you have lots of named export bindings. There is a wildcard import form where you import all of a module’s exports within a single namespace object, but there’s no way to wildcard import to top-level bindings.
Again, the ES6 module mechanism is intentionally designed to discourage modules with lots of exports; relatively speaking, it’s desired that such approaches be a little more difficult, as a sort of social engineering to encourage simple module design in favor of large/complex module design.
I would probably recommend you not mix default export with named exports, especially if you have a large API and refactoring to separate modules isn’t practical or desired. In that case, just use all named exports, and document that consumers of your module should probably use the import * as ..
(namespace import, discussed in the next section) approach to bring the whole API in at once on a single namespace.
We mentioned this earlier, but let’s come back to it in more detail. Other than the export default ...
form that exports an expression value binding, all other export forms are exporting bindings to local identifiers. For those bindings, if you change the value of a variable inside a module after exporting, the external imported binding will access the updated value:
var foo = 42;
export { foo as default };
export var bar = "hello world";
foo = 10;
bar = "cool";
When you import this module, the default
and bar
exports will be bound to the local variables foo
and bar
, meaning they will reveal the updated 10
and "cool"
values. The values at time of export are irrelevant. The values at time of import are irrelevant. The bindings are live links, so all that matters is what the current value is when you access the binding.
Warning: Two-way bindings are not allowed. If you import a foo
from a module, and try to change the value of your imported foo
variable, an error will be thrown! We’ll revisit that in the next section.
You can also re-export another module’s exports, such as:
export { foo, bar } from "baz";
export { foo as FOO, bar as BAR } from "baz";
export * from "baz";
Those forms are similar to just first importing from the "baz"
module then listing its members explicitly for export from your module. However, in these forms, the members of the "baz"
module are never imported to your module’s local scope; they sort of pass through untouched.
import
ing API Members
To import a module, unsurprisingly you use the import
statement. Just as export
has several nuanced variations, so does import
, so spend plenty of time considering the following issues and experimenting with your options.
If you want to import certain specific named members of a module’s API into your top-level scope, you use this syntax:
import { foo, bar, baz } from "foo";
Warning: The { .. }
syntax here may look like an object literal, or even an object destructuring syntax. However, its form is special just for modules, so be careful not to confuse it with other { .. }
patterns elsewhere.
The "foo"
string is called a module specifier. Because the whole goal is statically analyzable syntax, the module specifier must be a string literal; it cannot be a variable holding the string value.
From the perspective of your ES6 code and the JS engine itself, the contents of this string literal are completely opaque and meaningless. The module loader will interpret this string as an instruction of where to find the desired module, either as a URL path or a local filesystem path.
The foo
, bar
, and baz
identifiers listed must match named exports on the module’s API (static analysis and error assertion apply). They are bound as top-level identifiers in your current scope:
import { foo } from "foo";
foo();
You can rename the bound identifiers imported, as:
import { foo as theFooFunc } from "foo";
theFooFunc();
If the module has just a default export that you want to import and bind to an identifier, you can opt to skip the { .. }
surrounding syntax for that binding. The import
in this preferred case gets the nicest and most concise of the import
syntax forms:
import foo from "foo";
// or:
import { default as foo } from "foo";
Note: As explained in the previous section, the default
keyword in a module’s export
specifies a named export where the name is actually default
, as is illustrated by the second more verbose syntax option. The renaming from default
to, in this case, foo
, is explicit in the latter syntax and is identical yet implicit in the former syntax.
You can also import a default export along with other named exports, if the module has such a definition. Recall this module definition from earlier:
export default function foo() { .. }
export function bar() { .. }
export function baz() { .. }
To import that module’s default export and its two named exports:
import FOOFN, { bar, baz as BAZ } from "foo";
FOOFN();
bar();
BAZ();
The strongly suggested approach from ES6’s module philosophy is that you only import the specific bindings from a module that you need. If a module provides 10 API methods, but you only need two of them, some believe it wasteful to bring in the entire set of API bindings.
One benefit, besides code being more explicit, is that narrow imports make static analysis and error detection (accidentally using the wrong binding name, for instance) more robust.
Of course, that’s just the standard position influenced by ES6 design philosophy; there’s nothing that requires adherence to that approach.
Many developers would be quick to point out that such approaches can be more tedious, requiring you to regularly revisit and update your import
statement(s) each time you realize you need something else from a module. The trade-off is in exchange for convenience.
In that light, the preference might be to import everything from the module into a single namespace, rather than importing individual members, each directly into the scope. Fortunately, the import
statement has a syntax variation that can support this style of module consumption, called namespace import.
Consider a "foo"
module exported as:
export function bar() { .. }
export var x = 42;
export function baz() { .. }
You can import that entire API to a single module namespace binding:
import * as foo from "foo";
foo.bar();
foo.x; // 42
foo.baz();
Note: The * as ..
clause requires the *
wildcard. In other words, you cannot do something like import { bar, x } as foo from "foo"
to bring in only part of the API but still bind to the foo
namespace. I would have liked something like that, but for ES6 it’s all or nothing with the namespace import.
If the module you’re importing with * as ..
has a default export, it is named default
in the namespace specified. You can additionally name the default import outside of the namespace binding, as a top-level identifier. Consider a "world"
module exported as:
export default function foo() { .. }
export function bar() { .. }
export function baz() { .. }
And this import
:
import foofn, * as hello from "world";
foofn();
hello.default();
hello.bar();
hello.baz();
While this syntax is valid, it can be rather confusing that one method of the module (the default export) is bound at the top-level of your scope, whereas the rest of the named exports (and one called default
) are bound as properties on a differently named (hello
) identifier namespace.
As I mentioned earlier, my suggestion would be to avoid designing your module exports in this way, to reduce the chances that your module’s users will suffer these strange quirks.
All imported bindings are immutable and/or read-only. Consider the previous import; all of these subsequent assignment attempts will throw TypeError
s:
import foofn, * as hello from "world";
foofn = 42; // (runtime) TypeError!
hello.default = 42; // (runtime) TypeError!
hello.bar = 42; // (runtime) TypeError!
hello.baz = 42; // (runtime) TypeError!
Recall earlier in the “export
ing API Members” section that we talked about how the bar
and baz
bindings are bound to the actual identifiers inside the "world"
module. That means if the module changes those values, hello.bar
and hello.baz
now reference the updated values.
But the immutable/read-only nature of your local imported bindings enforces that you cannot change them from the imported bindings, hence the TypeError
s. That’s pretty important, because without those protections, your changes would end up affecting all other consumers of the module (remember: singleton), which could create some very surprising side effects!
Moreover, though a module can change its API members from the inside, you should be very cautious of intentionally designing your modules in that fashion. ES6 modules are intended to be static, so deviations from that principle should be rare and should be carefully and verbosely documented.
Warning: There are module design philosophies where you actually intend to let a consumer change the value of a property on your API, or module APIs are designed to be “extended” by having other “plug-ins” add to the API namespace. As we just asserted, ES6 module APIs should be thought of and designed as static and unchangeable, which strongly restricts and discourages these alternative module design patterns. You can get around these limitations by exporting a plain object, which of course can then be changed at will. But be careful and think twice before going down that road.
Declarations that occur as a result of an import
are “hoisted” (see the Scope & Closures title of this series). Consider:
foo();
import { foo } from "foo";
foo()
can run because not only did the static resolution of the import ..
statement figure out what foo
is during compilation, but it also “hoisted” the declaration to the top of the module’s scope, thus making it available throughout the module.
Finally, the most basic form of the import
looks like this:
import "foo";
This form does not actually import any of the module’s bindings into your scope. It loads (if not already loaded), compiles (if not already compiled), and evaluates (if not already run) the "foo"
module.
In general, that sort of import is probably not going to be terribly useful. There may be niche cases where a module’s definition has side effects (such as assigning things to the window
/global object). You could also envision using import "foo"
as a sort of preload for a module that may be needed later.
Circular Module Dependency
A imports B. B imports A. How does this actually work?
I’ll state off the bat that designing systems with intentional circular dependency is generally something I try to avoid. That having been said, I recognize there are reasons people do this and it can solve some sticky design situations.
Let’s consider how ES6 handles this. First, module "A"
:
import bar from "B";
export default function foo(x) {
if (x > 10) return bar( x - 1 );
return x * 2;
}
Now, module "B"
:
import foo from "A";
export default function bar(y) {
if (y > 5) return foo( y / 2 );
return y * 3;
}
These two functions, foo(..)
and bar(..)
, would work as standard function declarations if they were in the same scope, because the declarations are “hoisted” to the whole scope and thus available to each other regardless of authoring order.
With modules, you have declarations in entirely different scopes, so ES6 has to do extra work to help make these circular references work.
In a rough conceptual sense, this is how circular import
dependencies are validated and resolved:
- If the
"A"
module is loaded first, the first step is to scan the file and analyze all the exports, so it can register all those bindings available for import. Then it processes theimport .. from "B"
, which signals that it needs to go fetch"B"
. - Once the engine loads
"B"
, it does the same analysis of its export bindings. When it sees theimport .. from "A"
, it knows the API of"A"
already, so it can verify theimport
is valid. Now that it knows the"B"
API, it can also validate theimport .. from "B"
in the waiting"A"
module.
In essence, the mutual imports, along with the static verification that’s done to validate both import
statements, virtually composes the two separate module scopes (via the bindings), such that foo(..)
can call bar(..)
and vice versa. This is symmetric to if they had originally been declared in the same scope.
Now let’s try using the two modules together. First, we’ll try foo(..)
:
import foo from "foo";
foo( 25 ); // 11
Or we can try bar(..)
:
import bar from "bar";
bar( 25 ); // 11.5
By the time either the foo(25)
or bar(25)
calls are executed, all the analysis/compilation of all modules has completed. That means foo(..)
internally knows directly about bar(..)
and bar(..)
internally knows directly about foo(..)
.
If all we need is to interact with foo(..)
, then we only need to import the "foo"
module. Likewise with bar(..)
and the "bar"
module.
Of course, we can import and use both of them if we want to:
import foo from "foo";
import bar from "bar";
foo( 25 ); // 11
bar( 25 ); // 11.5
The static loading semantics of the import
statement mean that a "foo"
and "bar"
that mutually depend on each other via import
will ensure that both are loaded, parsed, and compiled before either of them runs. So their circular dependency is statically resolved and this works as you’d expect.
Module Loading
We asserted at the beginning of this “Modules” section that the import
statement uses a separate mechanism, provided by the hosting environment (browser, Node.js, etc.), to actually resolve the module specifier string into some useful instruction for finding and loading the desired module. That mechanism is the system Module Loader.
The default module loader provided by the environment will interpret a module specifier as a URL if in the browser, and (generally) as a local filesystem path if on a server such as Node.js. The default behavior is to assume the loaded file is authored in the ES6 standard module format.
Moreover, you will be able to load a module into the browser via an HTML tag, similar to how current script programs are loaded. At the time of this writing, it’s not fully clear if this tag will be <script type="module">
or <module>
. ES6 doesn’t control that decision, but discussions in the appropriate standards bodies are already well along in parallel of ES6.
Whatever the tag looks like, you can be sure that under the covers it will use the default loader (or a customized one you’ve pre-specified, as we’ll discuss in the next section).
Just like the tag you’ll use in markup, the module loader itself is not specified by ES6. It is a separate, parallel standard (http://whatwg.github.io/loader/) controlled currently by the WHATWG browser standards group.
At the time of this writing, the following discussions reflect an early pass at the API design, and things are likely to change.
Loading Modules Outside of Modules
One use for interacting directly with the module loader is if a non-module needs to load a module. Consider:
// normal script loaded in browser via `<script>`,
// `import` is illegal here
Reflect.Loader.import( "foo" ) // returns a promise for `"foo"`
.then( function(foo){
foo.bar();
} );
The Reflect.Loader.import(..)
utility imports the entire module onto the named parameter (as a namespace), just like the import * as foo ..
namespace import we discussed earlier.
Note: The Reflect.Loader.import(..)
utility returns a promise that is fulfilled once the module is ready. To import multiple modules, you can compose promises from multiple Reflect.Loader.import(..)
calls using Promise.all([ .. ])
. For more information about Promises, see “Promises” in Chapter 4.
You can also use Reflect.Loader.import(..)
in a real module to dynamically/conditionally load a module, where import
itself would not work. You might, for instance, choose to load a module containing a polyfill for some ES7+ feature if a feature test reveals it’s not defined by the current engine.
For performance reasons, you’ll want to avoid dynamic loading whenever possible, as it hampers the ability of the JS engine to fire off early fetches from its static analysis.
Customized Loading
Another use for directly interacting with the module loader is if you want to customize its behavior through configuration or even redefinition.
At the time of this writing, there’s a polyfill for the module loader API being developed (https://github.com/ModuleLoader/es6-module-loader). While details are scarce and highly subject to change, we can explore what possibilities may eventually land.
The Reflect.Loader.import(..)
call may support a second argument for specifying various options to customize the import/load task. For example:
Reflect.Loader.import( "foo", { address: "/path/to/foo.js" } )
.then( function(foo){
// ..
} )
It’s also expected that a customization will be provided (through some means) for hooking into the process of loading a module, where a translation/transpilation could occur after load but before the engine compiles the module.
For example, you could load something that’s not already an ES6-compliant module format (e.g., CoffeeScript, TypeScript, CommonJS, AMD). Your translation step could then convert it to an ES6-compliant module for the engine to then process.