JavaScript Modules for Including Functionality
The original JavaScript language did not have any concept of a module. All scripts ran in the same scope, and accessing a function defined in another script was done by referencing the global bindings created by that script. This actively encouraged accidental, hard-to-see entanglement of code and invited problems like unrelated scripts trying to use the same binding name.
Since ECMAScript 2015, JavaScript supports two different types of programs. Scripts behave in the old way: their bindings are defined in the global scope, and they have no way to directly reference other scripts. Modules get their own separate scope and support the import and export keywords, which aren't available in scripts, to declare their dependencies and interface. This module system is usually called ES modules (where ES stands for ECMAScript).
Modern JavaScript Modules
A modular program is composed of a number of such modules, wired together via their imports and exports.
The following example module converts between day names and numbers (as returned by Date's getDay method). It defines a constant that is not part of its interface, and two functions that are. It has no dependencies.
const names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; export function dayName(number) { return names[number]; } export function dayNumber(name) { return names.indexOf(name); }
The export
keyword can be put in front of a function, class, or binding definition to indicate that that binding is part of the module's interface. This makes it possible for other modules to use that binding by importing it.
import {dayName} from "./dayname.js"; let now = new Date(); console.log(`Today is ${dayName(now.getDay())}`); // → Today is Monday
The import
keyword, followed by a list of binding names in braces, makes bindings from another module available in the current module. Modules are identified by quoted strings.
How such a module name is resolved to an actual program differs by platform. The browser treats them as web addresses, whereas Node.js
resolves them to files. When you run a module, all the other modules it depends on—and the modules those depend on—are loaded, and the exported bindings are made available to the modules that import them.
Import and export declarations cannot appear inside of functions, loops, or other blocks. They are immediately resolved when the module is loaded, regardless of how the code in the module executes. To reflect this, they must appear only in the outer module body.
A module's interface thus consists of a collection of named bindings, which other modules that depend on the module can access. Imported bindings can be renamed to give them a new local name using as
after their name.
import {dayName as nomDeJour} from "./dayname.js"; console.log(nomDeJour(3)); // → Wednesday
A module may also have a special export named default
, which is often used for modules that only export a single binding. To define a default export, write export default
before an expression, a function declaration, or a class declaration.
export default ["Winter", "Spring", "Summer", "Autumn"];
Such a binding is imported by omitting the braces around the name of the import:
import seasonNames from "./seasonname.js";
To import all bindings from a module at the same time, you can use import *
. You provide a name, and that name will be bound to an object holding all the module's exports. This can be useful when you are using a lot of different exports.
import * as dayName from "./dayname.js"; console.log(dayName.dayName(3)); // → Wednesday
Dynamic Imports (ES6)
Dynamic imports allow you to load JavaScript modules at runtime, which is useful for lazy loading or conditionally including files.
Example:
// Dynamically import the module import('./math.mjs').then((module) => { console.log(module.add(2, 3)); // Output: 5 console.log(module.pi); // Output: 3.14159 });
Key Notes:
import()
returns a promise that resolves to the module.- Useful for optimizing performance in large applications by loading code only when needed.
Packages and NPM
We need a place to store and find packages and a convenient way to install and upgrade them. In the JavaScript world, this infrastructure is provided by NPM (https://www.npmjs.com).
NPM is two things: an online service where you can download (and upload) packages, and a program (bundled with Node.js) that helps you install and manage them.
At the time of writing, there are more than three million different packages available on NPM. A large portion of those are rubbish, to be fair. But almost every useful, publicly available JavaScript package can be found on NPM. For example, an INI file parser is available under the package name ini.
Having quality packages available for download is extremely valuable. It means that we can often avoid reinventing a program that 100 people have written before and get a solid, well-tested implementation at the press of a few keys.
Software is cheap to copy, so once someone has written it, distributing it to other people is an efficient process. Writing it in the first place is hard work, though, and responding to people who have found problems in the code or who want to propose new features is even more work.
By default, you own the copyright to the code you write, and other people may use it only with your permission. But because some people are just nice and because publishing good software can help make you a little bit famous among programmers, many packages are published under a license that explicitly allows other people to use it.
Most code on NPM is licensed this way. Some licenses require you to also publish code that you build on top of the package under the same license. Others are less demanding, requiring only that you keep the license with the code as you distribute it. The JavaScript community mostly uses the latter type of license. When using other people's packages, make sure you are aware of their licenses.
Now, instead of writing our own INI file parser, we can use one from NPM:
import {parse} from "ini"; console.log(parse("x = 10\ny = 20")); // → {x: "10", y: "20"}
CommonJS Modules
Before 2015, when the JavaScript language had no built-in module system, people were already building large systems in JavaScript. To make that workable, they needed modules.
The community designed its own improvised module systems on top of the language. [...]
If we implement our own module loader, we can do better. The most widely used approach to bolted-on JavaScript modules is called CommonJS modules. Node.js used this module system from the start (though it now also knows how to load ES modules), and it is the module system used by many packages on NPM.
A CommonJS module looks like a regular script, but it has access to two bindings that it uses to interact with other modules. The first is a function called require
. When you call this with the module name of your dependency, it makes sure the module is loaded and returns its interface. The second is an object named exports
, which is the interface object for the module. It starts out empty and you add properties to it to define exported values.
This CommonJS example module provides a date-formatting function. It uses two packages from NPM—ordinal to convert numbers to strings like 1st
and 2nd
, and date-names to get the English names for weekdays and months. It exports a single function, formatDate
, which takes a Date
object and a template string.
The template string may contain codes that direct the format, such as YYYY for the full year and Do for the ordinal day of the month. You could give it a string like “MMMM Do YYYY” to get output like November 22nd 2017.
const ordinal = require("ordinal"); const {days, months} = require("date-names"); exports.formatDate = function(date, format) { return format.replace(/YYYY|M(MMM)?|Do?|dddd/g, tag =>{ if (tag == "YYYY") return date.getFullYear(); if (tag == "M") return date.getMonth(); if (tag == "MMMM") return months[date.getMonth()]; if (tag == "D") return date.getDate(); if (tag == "Do") return ordinal(date.getDate()); if (tag == "dddd") return days[date.getDay()]; }); };