Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon

Modular Programming in ECMAScript 6

Save for later
  • 18 min read
  • 15 Feb 2016

article-image

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:

  • What is modular programming?
  • The benefits of modular programming
  • The basics of IIFE modules, AMD, UMD, and CommonJS
  • Creating and importing the ES6 modules
  • The basics of the Modular Loader
  • Creating a basic JavaScript library using modules

(For more resources related to this topic, see here.)

The JavaScript modules in a nutshell

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:

  • It keeps our code both cleanly separated and organized by splitting into multiple modules
  • Modular programming leads to fewer global variables, that is, it eliminates the problem of global variables, because modules don't interface via the global scope, and each module has its own scope
  • Makes code reusability easier as importing and using the same modules in different projects is easier
  • It allows many programmers to collaborate on the same program or library, by making each programmer to work on a particular module with a particular functionality
  • Bugs in an application can easily be easily identified as they are localized to a particular module

Implementing modules – the old way

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 Immediately-Invoked Function Expression

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.

Asynchronous Module Definition

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

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.

Universal Module Definition

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.

Implementing modules – the new way

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.

Creating the ES6 modules

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:

  • The first format exports a variable.
  • The second format is used to export multiple variables.
  • The third format is used to export a variable with another name, that is, an alias.
  • The fourth format is used to export multiple variables with different names.
  • The fifth format uses default as the alias. We will find out the use of this later in this article.
  • The sixth format is similar to fourth format, but it also has the default alias.
  • The seventh format works similar to fifth format, but here you can place an expression instead of a variable name.
  • The eighth format is used to export the exported variables of a submodule.
  • The ninth format is used to export all the exported variables of a submodule.

Here are some important things that you need to know about the export statement:

  • An export statement can be used anywhere in a module. It's not compulsory to use it at the end of the module.
  • There can be any number of export statements in a module.
  • Unlock access to the largest independent learning library in Tech for FREE!
    Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
    Renews at €18.99/month. Cancel anytime
  • You cannot export variables on demand. For example, placing the export statement in the if…else condition throws an error. Therefore, we can say that the module structure needs to be static, that is, exports can be determined on compile time.
  • You cannot export the same variable name or alias multiple times. But you can export a variable multiple times with a different alias.
  • All the code inside a module is executed in the strict mode by default.
  • The values of the exported variables can be changed inside the module that exported them.

Importing the ES6 modules

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:

  • In the first format, the default alias is imported. The x is alias of the default alias.
  • In the second format, the x variable is imported.
  • The third format is the same as the second format. It's just that x2 is an alias of x1.
  • In the fourth format, we import the x1 and x2 variables.
  • In the fifth format, we import the x1 and x2 variables. The x3 is an alias of the x2 variable.
  • In the sixth format, we import the x1 and x2 variable, and the default alias. The x is an alias of the default alias.
  • In the seventh format, we just import the module. We do not import any of the variables exported by the module.
  • In the eighth format, we import all the variables, and wrap them in an object called x. Even the default alias is imported.
  • The ninth format is the same as the eighth format. Here, we give another alias to the default alias.[RR1] 

Here are some important things that you need to know about the import statement:

  • While importing a variable, if we import it with an alias, then to refer to that variable, we have to use the alias and not the actual variable name, that is, the actual variable name will not be visible, only the alias will be visible.
  • The import statement doesn't import a copy of the exported variables; rather, it makes the variables available in the scope of the program that imports it. Therefore, if you make a change to an exported variable inside the module, then the change is visible to the program that imports it.
  • The imported variables are read-only, that is, you cannot reassign them to something else outside of the scope of the module that exports them.
  • A module can only be imported once in a single instance of a JavaScript engine. If we try to import it again, then the already imported instance of the module will be used.
  • We cannot import modules on demand. For example, placing the import statement in the if…else condition throws an error. Therefore, we can say that the imports should be able to be determined on compile time.
  • The ES6 imports are faster than the AMD and CommonJS imports, because the ES6 imports are supported natively and also as importing modules and exporting variables are not decided on demand. Therefore, it makes JavaScript engine easier to optimize performance.

The module loader

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.

Using modules in browsers

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.

Using modules in the eval() function

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.

The default exports vs. the named exports

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.

Diving into an example

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:

  • Create a file named math.js, and a directory named math_modules. Inside the math_modules directory, create two files named logarithm.js and trigonometry.js, respectively.

    Here, the math.js file is the root module, whereas the logarithm.js and the trigonometry.js files are its submodules.

  • Place this code inside the logarithm.js file:
    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));

Summary

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:

  • JavaScript at Scale (https://www.packtpub.com/web-development/javascript-scale)
  • Google Apps Script for Beginners (https://www.packtpub.com/web-development/google-apps-script-beginners)
  • Learning TypeScript (https://www.packtpub.com/web-development/learning-typescript)
  • JavaScript Concurrency (https://www.packtpub.com/web-development/javascript-concurrency)

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.

Resources for Article:

 


Further resources on this subject: