Modules

In JavaScript, a module is a self-contained unit of code that can be reused and imported into other JavaScript files or modules. Modules help organize code by encapsulating related functionality into separate files, making it easier to maintain and understand complex applications.

Code encapsulation

Classes can act as modules, encapsulating variables and methods from the global namespace of a program. However, this technique does not allow us a way to hide internal implementation details inside the module.

Another approach to modularity is using immediately invoked function expressions that leave the implementation details and utility functions hidden within the enclosing function but making the public API of the module the return value of the function.

const stats = (function() {
  // Utility functions private to the module
  const sum = (x,y) => x + y;
  const square = x => x * x;

  // A public function that will be exported
  function mean(data) {
    return data.reduce(sum)/data.length;
  }

   // A public function that will be exported
   function stddev(data) {
    let m = mean(data);
    return Math.sqrt(
      data.map(x => x-m).map(square).reduce(sum)/(data.length-1)
    );
   }

   // Export public functions as properties of an object
   return { mean, stddev };
}());

// Use the module
stats.mean([1,3,5,7,9]); // => 5
stats.stddev([1,3,5,7,9]); // => 3.1622776601683795

Native ES6 modules

ES6 adds import and export keywords to JavaScript to support modularity as a core language feature. With modules, each file has its own private context. Code inside a module is automatically in strict mode, just like any class definitions. The native modules in JavaScript starting from ES6 are added to HTML pages using a script tag with a special attribute type="module".

<script type="module" src="modules.js"></script>

To export a constant, a variable, a function, or a class from a JavaScript module, add the keyword export before the declaration. The export keyword can be used with declarations that have a name that appear at the top level of your JavaScript code.

// modules/functions.js
function not(f) {
  return function(...args) {
    let result = f.apply(this, args);
    return !result;
  };
}

export const even = x => x%2 === 0;
export const odd = not(even);

You import values that have been exported by other modules with the import keyword. Imports can only appear at the top level of a module and are hoisted to the top. You can import everything to a named object that contains all imported values as its properties.

import * as f from './modules/functions.js';
let odd = [1,1,3,5,5,7].every(f.odd);

You can also define your constants, variables, functions, and classes normally without export keyword, and then write a single export statement at the end. The curly braces that follow export keyword, however, do not define an object literal.

// modules/stats.js
const sum = (x,y) => x + y;
const square = x => x * x;

function mean(data) {
  return data.reduce(sum)/data.length;
}

function stddev(data) {
  let m = mean(data);
  return Math.sqrt(
    data.map(x => x-m).map(square).reduce(sum)/(data.length-1)
  );
}

export  { mean, stddev };

You can import any subset of exported values by listing their names within curly braces. The identifier to which the imported value is assigned to behaves like a constant.

import { mean, stddev } from './modules/stats.js';
let m = mean([1,3,5,7,9]);
let sd = stddev([1,3,5,7,9]);

If the module exports a single value, such as a class or a function, you can use export default keyword to export any expression, including anonymous function and class expressions.

// modules/shapes.js
const PI = Math.PI;
function degreesToRadians(d) { return d * PI / 180; }

export default class Circle {
  constructor(r) { this.r = r; }
  area() { return PI * this.r * this.r; }
}

You can import a default export from another module as a specified identifier to the current module.

import Circle from './modules/shapes.js';
let circle = new Circle(5);
let area = circle.area();

Lastly, you can import a module that does not have any exports. In this case, the import statement might still execute any code within the module (like global variable declarations or side effects), but you won't be able to access any variables, functions, or objects exported by the module. For example, an analytics module for a web application might run code to register various event handlers and then use those event handlers to send telemetry data back to the server at appropriate times.

import './modules/analytics.js';

Within ES6 module, you can get the URL of the currently executing module by using import.meta object. For example, you can create a URL for a file relative to the directory where your module is located.

let url = new URL('file.txt', import.meta.url);

Dynamic imports

Static imports may lead to longer initial load times, especially for larger applications on the web. Dynamic imports allow for lazy-loading, where modules are loaded only when they are needed, improving performance. To load a module dynamically, you pass a module specifier to the import() operator. If the module is loaded successfully, the promise returned by import() is resolved, and the then block is executed, which produces the object like the one you would get with import * as form of the static import statement. If an error occurs while loading the module, the promise is rejected, and the catch block is executed.

import("./modules/stats.js")
  .then(mstats => {
    let average = mstats.mean([1,3,5,7,9]);
    console.log(`Average: ${average}`);
  })
  .catch(error => {
    console.log('Error loading module:', error);
  });

You can also define an asynchronous function to load the module dynamically with await.

async function analyzeData(data) {
  try {
    let kstats = await import("./modules/stats.js");
    return {
      average: kstats.mean(data),
      stddev: kstats.stddev(data)
    };
  } catch (error) {
    console.log('Error loading module:', error);
    return { average: null, stddev: null };
  }
}

let result = await analyzeData([1,3,5,7,9]);

Re-exports

You can import individual modules and export them again as a bundle for convenience. Use a wildcard to re-export all of the named values from another module. You can rename the re-exported identifiers by using the as keyword.

export * from "./modules/stats/stddev.js";
export { mean, mean as average } from "./modules/stats/mean.js";
export { default as variance } from "./modules/stats/variance.js";

Modules in Node

In Node, each file is an independent module with a private namespace by default. Constants, variables, functions, and classes defined in one file are private to that file unless the file exports them. Values exported by one module are only visible in another module if that module explicitly imports them. To export multiple values, simply assign them to the properties of the global exports object.

exports.PI = Math.PI;
exports.degreesToRadians = function(d) { return d * PI / 180; }

To define a module that exports only a single function or class, assign the single value you want to export to module.exports, which is the same object that exports refers to.

module.exports = x => x%2 === 0;

Another approach is to export a single object at the end of the module.

const sum = (x,y) => x + y;
const square = x => x * x;

const mean = data => data.reduce(sum)/data.length;
const stddev = function(data) {
  let m = mean(data);
  return Math.sqrt(
    data.map(x => x-m).map(square).reduce(sum)/(data.length-1)
  );
};

module.exports = { mean, stddev };

A Node module imports another module by calling the require() function. The argument to this function is the name of the module to be imported, and the return value is whatever value that module exports. If the module is built in to Node or installed on your system via a package manager, then use the unqualified name of the module. If you import a module of your own code, the module name should be a relative or absolute path to the file that contains the code of the module. You can use destructuring assignment to import the specific properties of the object that you plan to use.

const http = require('http'); // import built-in module
const stats = require('./stats.js'); // import module of your own code
const { stddev } = require('./stats.js'); // import specific properties