The concept of modules comes from the modular programming paradigm. This paradigm proposes that software should be composed of separate, interchangeable components called “modules” by breaking down program functions into stand-alone files that can work separately or coupled in an application.

A module is a stand-alone file that encapsulates code to implement certain functionality and promote reusability and organization.

Here you will cover the module systems used in JavaScript applications, including the module pattern, the CommonJS module system used in most Node.js applications, and the ES6 Module system.

The Module Pattern

Before the introduction of native JavaScript modules, the module design pattern was used as a module system to scope variables and functions to a single file.

This was implemented using immediately invoked function expressions, popularly known as IIFEs. An IIFE is an un-reusable function that runs as soon as it is created.

Here’s the basic structure of an IIFE:

        (function () {
  //code here
})();
  
(() => {
  //code here
})();
  
(async () => {
  //code here
})();

The code block above describes IIFEs used in three different contexts.

IIFEs were used because variables declared inside a function are scoped to the function, making them only accessible inside the function, and because functions allow you to return data (making them publicly accessible).

For example:

        const foo = (function () {
  const sayName = (name) => {
    console.log(`Hey, my name is ${name}`);
  };

  //Exposing the variables
  return {
    callSayName: (name) => sayName(name),
  };
})();
//Accessing exposed methods
foo.callSayName("Bar");

The code block above is an example of how modules were created before the introduction of native JavaScript modules.

The code block above contains an IIFE. The IIFE contains a function that it makes accessible by returning it. All the variables declared in the IIFE are protected from the global scope. Thus, the method (sayName) is only accessible through the public function, callSayName.

Notice that the IIFE is saved to a variable, foo. This is because, without a variable pointing to its location in memory, the variables will be inaccessible after the script runs. This pattern is possible due to JavaScript closures.

The CommonJS Module System

The CommonJS module system is a module format defined by the CommonJS group to solve JavaScript scope issues by executing each module in its namespace.

The CommonJS module system works by forcing modules to explicitly export variables they want to expose to other modules.

This module system was created for server-side JavaScript (Node.js) and, as such, is not supported by default in browsers.

To implement CommonJS modules in your project, you have to first initialize NPM in your application by running:

        npm init -y

Variables exported following the CommonJS module system can be imported like so:

        //randomModule.js

//installed package
const installedImport = require("package-name");

//local module
const localImport = require("/path-to-module");

Modules are imported in CommonJS using the require statement, which reads a JavaScript file, executes the read file, and returns the exports object. The exports object contains all the available exports in the module.

You can export a variable following the CommonJS module system using either named exports or default exports.

Named Exports

Named exports are exports identified by the names they were assigned. Named exports allow multiple exports per module, unlike default exports.

For example:

        //main.js

exports.myExport = function () {
  console.log("This is an example of a named export");
};

exports.anotherExport = function () {
  console.log("This is another example of a named export");
};

In the code block above, you are exporting two named functions (myExport and anotherExport) by attaching them to the exports object.

Similarly, you can export the functions like so:

        const myExport = function () {
  console.log("This is an example of a named export");
};

const anotherExport = function () {
  console.log("This is another example of a named export");
};

module.exports = {
  myExport,
  anotherExport,
};

In the code block above, you set the exports object to the named functions. You can only assign the exports object to a new object through the module object.

Your code would throw an error if you attempted to do it this way:

        //wrong way
exports = {
  myExport,
  anotherExport,
};

There are two ways you can import named exports:

1. Import all the exports as a single object and access them separately using the dot notation.

For example:

        //otherModule.js

const foo = require("./main");

foo.myExport();
foo.anotherExport();

2. De-structure the exports from the exports object.

For example:

        //otherModule.js

const { myExport, anotherExport } = require("./main");

myExport();
anotherExport();

One thing is common in all the methods of importing, they must be imported using the same names they were exported with.

Default Exports

A default export is an export identified by any name of your choice. You can only have one default export per module.

For example:

        //main.js
class Foo {
  bar() {
    console.log("This is an example of a default export");
  }
}

module.exports = Foo;

In the code block above, you are exporting a class (Foo) by reassigning the exports object to it.

Importing default exports is similar to importing named exports, except that you can use any name of your choice to import them.

For example:

        //otherModule.js
const Bar = require("./main");

const object = new Bar();

object.bar();

In the code block above, the default export was named Bar, although you can use any name of your choice.

The ES6 Module System

ECMAScript Harmony module system, popularly known as ES6 modules, is the official JavaScript module system.

ES6 modules are supported by browsers and servers, although you require a bit of configuration before using them.

In browsers, you have to specify the type as module in the script import tag.

Like so:

        //index.html
<script src="./app.js" type="module"></script>

In Node.js, you have to set type to module in your package.json file.

Like so:

        //package.json
"type":"module"

You can also export variables using the ES6 module system using either named exports or default exports.

Named Exports

Similar to named imports in CommonJS modules, they are identified by the names they were assigned and allow multiple exports per module.

For example:

        //main.js
export const myExport = function () {
  console.log("This is an example of a named export");
};

export const anotherExport = function () {
  console.log("This is another example of a named export");
};

In the ES6 module system, named exports are exported by prefixing the variable with the export keyword.

Named exports can be imported into another module in ES6 in the same ways as CommonJS:

  • De-structuring the required exports from the exports object.
  • Importing all the exports as a single object and accessing them separately using the dot notation.

Here’s an example of de-structuring:

        //otherModule.js
import { myExport, anotherExport } from "./main.js";

myExport()
anotherExport()

Here’s an example of importing the whole object:

        import * as foo from './main.js'

foo.myExport()
foo.anotherExport()

In the code block above, the asterisk (*) means “all”. The as keyword assigns the exports object to the string that follows it, in this case, foo.

Default Exports

Similar to default exports in CommonJS, they are identified by any name of your choice, and you can only have one default export per module.

For example:

        //main.js
class Foo {
  bar() {
    console.log("This is an example of a default export");
  }
}

export default Foo;

Default exports are created by adding the default keyword after the export keyword, followed by the name of the export.

Importing default exports is similar to importing named exports, except that you can use any name of your choice to import them.

For example:

        //otherModule.js
import Bar from "./main.js";

Mixed Exports

The ES6 module standard allows you to have both default exports and named exports in one module, unlike CommonJS.

For example:

        //main.js
export const myExport = function () {
  console.log("This is another example of a named export");
};

class Foo {
  bar() {
    console.log("This is an example of a default export");
  }
}

export default Foo;

Importance of Modules

Dividing your code into modules not only makes them easier to read but it makes it more reusable and also maintainable. Modules in JavaScript also make your code less error-prone, as all modules are executed in strict mode by default.