Modular programming is one of the most important and frequently used software design techniques. Unfortunately, JavaScript didn't support modules natively that lead JavaScript programmers to use alternative techniques to achieve modular programming in JavaScript. But now, ES6 brings modules into JavaScript officially.
This article is all about how to create and import JavaScript modules. In this article, we will first learn how the modules were created earlier, and then we will jump to the new built-in module system that was introduced in ES6, known as the ES6 modules.
In this article, we'll cover:
(For more resources related to this topic, see here.)
The practice of breaking down programs and libraries into modules is called modular programming.
In JavaScript, a module is a collection of related objects, functions, and other components of a program or library that are wrapped together and isolated from the scope of the rest of the program or library.
A module exports some variables to the outside program to let it access the components wrapped by the module. To use a module, a program needs to import the module and the variables exported by the module.
A module can also be split into further modules called as its submodules, thus creating a module hierarchy.
Modular programming has many benefits. Some benefits are:
Before ES6, JavaScript had never supported modules natively. Developers used other techniques and third-party libraries to implement modules in JavaScript.
Using Immediately-invoked function expression (IIFE), Asynchronous Module Definition (AMD), CommonJS, and Universal Module Definition (UMD) are various popular ways of implementing modules in ES5. As these ways were not native to JavaScript, they had several problems. Let's see an overview of each of these old ways of implementing modules.
The IIFE is used to create an anonymous function that invokes itself. Creating modules using IIFE is the most popular way of creating modules.
Let's see an example of how to create a module using IIFE:
//Module Starts
(function(window){
var sum = function(x, y){
return x + y;
}
var sub = function(x, y){
return x - y;
}
var math = {
findSum: function(a, b){
return sum(a,b);
},
findSub: function(a, b){
return sub(a, b);
}
}
window.math = math;
})(window)
//Module Ends
console.log(math.findSum(1, 2)); //Output "3"
console.log(math.findSub(1, 2)); //Output "-1"
Here, we created a module using IIFE. The sum and sub variables are global to the module, but not visible outside of the module. The math variable is exported by the module to the main program to expose the functionalities that it provides.
This module works completely independent of the program, and can be imported by any other program by simply copying it into the source code, or importing it as a separate file.
A library using IIFE, such as jQuery, wraps its all of its APIs in a single IIFE module. When a program uses a jQuery library, it automatically imports the module.
AMD is a specification for implementing modules in browser. AMD is designed by keeping the browser limitations in mind, that is, it imports modules asynchronously to prevent blocking the loading of a webpage. As AMD is not a native browser specification, we need to use an AMD library. RequireJS is the most popular AMD library.
Let's see an example on how to create and import modules using RequireJS. According to the AMD specification, every module needs to be represented by a separate file. So first, create a file named math.js that represents a module. Here is the sample code that will be inside the module:
define(function(){
var sum = function(x, y){
return x + y;
}
var sub = function(x, y){
return x - y;
}
var math = {
findSum: function(a, b){
return sum(a,b);
},
findSub: function(a, b){
return sub(a, b);
}
}
return math;
});
Here, the module exports the math variable to expose its functionality.
Now, let's create a file named index.js, which acts like the main program that imports the module and the exported variables. Here is the code that will be inside the index.js file:
require(["math"], function(math){
console.log(math.findSum(1, 2)); //Output "3"
console.log(math.findSub(1, 2)); //Output "-1"
})
Here, math variable in the first parameter is the name of the file that is treated as the AMD module. The .js extension to the file name is added automatically by RequireJS.
The math variable, which is in the second parameter, references the exported variable.
Here, the module is imported asynchronously, and the callback is also executed asynchronously.
CommonJS is a specification for implementing modules in Node.js. According to the CommonJS specification, every module needs to be represented by a separate file. The CommonJS modules are imported synchronously.
Let's see an example on how to create and import modules using CommonJS. First, we will create a file named math.js that represents a module. Here is a sample code that will be inside the module:
var sum = function(x, y){
return x + y;
}
var sub = function(x, y){
return x - y;
}
var math = {
findSum: function(a, b){
return sum(a,b);
},
findSub: function(a, b){
return sub(a, b);
}
}
exports.math = math;
Here, the module exports the math variable to expose its functionality.
Now, let's create a file named index.js, which acts like the main program that imports the module. Here is the code that will be inside the index.js file:
var math = require("./math").math;
console.log(math.findSum(1, 2)); //Output "3"
console.log(math.findSub(1, 2)); //Output "-1"
Here, the math variable is the name of the file that is treated as module. The .js extension to the file name is added automatically by CommonJS.
We saw three different specifications of implementing modules. These three specifications have their own respective ways of creating and importing modules. Wouldn't it have been great if we can create modules that can be imported as an IIFE, AMD, or CommonJS module?
UMD is a set of techniques that is used to create modules that can be imported as an IIFE, CommonJS, or AMD module. Therefore now, a program can import third-party modules, irrespective of what module specification it is using.
The most popular UMD technique is returnExports. According to the returnExports technique, every module needs to be represented by a separate file. So, let's create a file named math.js that represents a module. Here is the sample code that will be inside the module:
(function (root, factory) {
//Environment Detection
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.returnExports = factory();
}
}(this, function () {
//Module Definition
var sum = function(x, y){
return x + y;
}
var sub = function(x, y){
return x - y;
}
var math = {
findSum: function(a, b){
return sum(a,b);
},
findSub: function(a, b){
return sub(a, b);
}
}
return math;
}));
Now, you can successfully import the math.js module any way that you wish, for instance, by using CommonJS, RequireJS, or IIFE.
ES6 introduced a new module system called ES6 modules. The ES6 modules are supported natively and therefore, they can be referred as the standard JavaScript modules.
You should consider using ES6 modules instead of the old ways, because they have neater syntax, better performance, and many new APIs that are likely to be packed as the ES6 modules.
Let's have a look at the ES6 modules in detail.
Every ES6 module needs to be represented by a separate .js file. An ES6 module can contain any JavaScript code, and it can export any number of variables.
A module can export a variable, function, class, or any other entity.
We need to use the export statement in a module to export variables. The export statement comes in many different formats. Here are the formats:
export {variableName};
export {variableName1, variableName2, variableName3};
export {variableName as myVariableName};
export {variableName1 as myVariableName1, variableName2 as
myVariableName2};
export {variableName as default};
export {variableName as default, variableName1 as myVariableName1,
variableName2};
export default function(){};
export {variableName1, variableName2} from "myAnotherModule";
export * from "myAnotherModule";
Here are the differences in these formats:
Here are some important things that you need to know about the export statement:
To import a module, we need to use the import statement. The import statement comes in many different formats. Here are the formats:
import x from "module-relative-path";
import {x} from "module-relative-path";
import {x1 as x2} from "module-relative-path";
import {x1, x2} from "module-relative-path";
import {x1, x2 as x3} from "module-relative-path";
import x, {x1, x2} from "module-relative-path";
import "module-relative-path";
import * as x from "module-relative-path";
import x1, * as x2 from "module-relative-path";
An import statement consists of two parts: the variable names we want to import and the relative path of the module.
Here are the differences in these formats:
Here are some important things that you need to know about the import statement:
A module loader is a component of a JavaScript engine that is responsible for importing modules.
The import statement uses the build-in module loader to import modules.
The built-in module loaders of the different JavaScript environments use different module loading mechanisms. For example, when we import a module in JavaScript running in the browsers, then the module is loaded from the server. On the other hand, when we import a module in Node.js, then the module is loaded from filesystem.
The module loader loads modules in a different manner, in different environments, to optimize the performance. For example, in the browsers, the module loader loads and executes modules asynchronously in order to prevent the importing of the modules that block the loading of a webpage.
You can programmatically interact with the built-in module loader using the module loader API to customize its behavior, intercept module loading, and fetch the modules on demand.
We can also use this API to create our own custom module loaders.
The specifications of the module loader are not specified in ES6. It is a separate standard, controlled by the WHATWG browser standard group. You can find the specifications of the module loader at http://whatwg.github.io/loader/.
The ES6 specifications only specify the import and export statements.
The code inside the <script> tag doesn't support the import statement, because the tag's synchronous nature is incompatible with the asynchronicity of the modules in browsers. Instead, you need to use the new <module> tag to import modules.
Using the new <module> tag, we can define a script as a module. Now, this module can import other modules using the import statement.
If you want to import a module using the <script> tag, then you have to use the Module Loader API.
The specifications of the <module> tag are not specified in ES6.
You cannot use the import and export statements in the eval() function. To import modules in the eval() function, you need to use the Module Loader API.
When we export a variable with the default alias, then it's called as a default export. Obviously, there can only be one default export in a module, as an alias can be used only once.
All the other exports except the default export are called as named exports.
It's recommended that a module should either use default export or named exports. It's not a good practice to use both together.
The default export is used when we want to export only one variable. On the other hand, the named exports are used when we want to export the multiple variables.
Let's create a basic JavaScript library using the ES6 modules. This will help us understand how to use the import and export statements. We will also learn how a module can import other modules.
The library that we will create is going to be a math library, which provides basic logarithmic and trigonometric functions. Let's get started with creating our library:
Here, the math.js file is the root module, whereas the logarithm.js and the trigonometry.js files are its submodules.
var LN2 = Math.LN2;
var N10 = Math.LN10;
function getLN2()
{
return LN2;
}
function getLN10()
{
return LN10;
}
export {getLN2, getLN10};
Here, the module is exporting the functions named as exports.
It's preferred that the low-level modules in a module hierarchy should export all the variables separately, because it may be possible that a program may need just one exported variable of a library. In this case, a program can import this module and a particular function directly. Loading all the modules when you need just one module is a bad idea in terms of performance.
Similarly, place this code in the trigonometry.js file:
var cos = Math.cos;
var sin = Math.sin;
function getSin(value)
{
return sin(value);
}
function getCos(value)
{
return cos(value);
}
export {getCos, getSin};
Here we do something similar. Place this code inside the math.js file, which acts as the root module:
import * as logarithm from "math_modules/logarithm";
import * as trigonometry from "math_modules/trigonometry";
export default {
logarithm: logarithm,
trigonometry: trigonometry
}
It doesn't contain any library functions. Instead, it makes easy for a program to import the complete library. It imports its submodules, and then exports their exported variables to the main program.
Here, in case the logarithm.js and trigonometry.js scripts depends on other submodules, then the math.js module shouldn't import those submodules, because logarithm.js and trigonometry.js are already importing them.
Here is the code using which a program can import the complete library:
import math from "math";
console.log(math.trigonometry.getSin(3));
console.log(math.logarithm.getLN2(3));
In this article, we saw what modular programming is and learned different modular programming specifications. We also saw different ways to create modules using JavaScript. Technologies such as the IIFE, CommonJS, AMD, UMD, and ES6 modules are covered. Finally, we created a basic library using the modular programming design technique. Now, you should be confident enough to build the JavaScript apps using the ES6 modules.
To learn more about ECMAScript and JavaScript, the following books published by Packt Publishing (https://www.packtpub.com/) are recommended:
You can also watch out for an upcoming title, Mastering JavaScript Object-Oriented Programming, on this technology on Packt Publishing's website at https://www.packtpub.com/web-development/mastering-javascript-object-oriented-programming.
Further resources on this subject: