From IIFE to ES Modules: The Evolution of JavaScript Module Systems

An in‑depth look at JavaScript’s module journey—from early script tags and IIFE patterns, through CommonJS, AMD, CMD, UMD, to modern ES Modules—explaining each specification, its motivations, code examples, and how they shaped today’s front‑end development.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
From IIFE to ES Modules: The Evolution of JavaScript Module Systems

Introduction

Review the development of JavaScript from a simple browser scripting language to a modern programming language for building web applications, highlighting the key role of modular technology. This article examines JavaScript’s modular exploration and the evolution of modular specifications, reviewing the history of JavaScript modularization.

Modular Concept

Before reviewing the evolution of JavaScript modular technology, we first define basic concepts. A module, according to Wikipedia, is a component composed of several basic functional units that can be combined to form a complete system, device, or program. Modules usually share the same process or logic, and changing their components can adjust functionality or purpose.

Module is a specific functional component composed of several basic functional units, used to build a complete system, device, or program. Modules typically share the same process or logic, and modifying their components can adapt their function or usage.

In simple terms, a module can be understood as an independent file or script that contains specific functionality or logic. Modules separate code into independent units, facilitating management, reuse, and maintenance.

What is modularization?

Modularization is a core principle of modern software engineering that improves maintainability and extensibility by breaking large, complex systems into smaller, manageable functional modules with clear interfaces and responsibilities.

Modularization allows independent development, testing, and maintenance of modules, and enables reuse across projects, significantly improving development efficiency and code quality in large applications.

JavaScript Modular Exploration

Historically, JavaScript lacked a modular concept for a long time. Initially, developers wrote JS code directly inside script tags:

<script>

function add(a, b) {
    return a + b;
};

add(1,2);
</script>

As code grew, logic was split into multiple JS files:

/* a.js */
function add(a, b) {
    return a + b;
}

/* b.js */
function average(a, b) {
    return add(a, b) / 2;
}

/* main.js */
var result = average(5, 10);
console.log(result);

These files were then included in HTML:

<script src="./a.js"></script>
<script src="./b.js"></script>
<script src="./main.js"></script>

All functions and variables were exposed to the global scope, causing naming conflicts and making dependency management difficult.

Namespace

To mitigate naming conflicts, the namespace concept emerged:

var moduleA = {
    name = 'moduleA'
}
moduleA.add = function(a, b) {
    console.log(a + b)
}
moduleA.add(1, 2)

While namespaces reduced conflicts, they still exposed internal content and lacked security.

Immediately Invoked Function Expression (IIFE)

Immediately invoked anonymous closures are the foundation of modular implementation.

IIFE creates an isolated scope, preventing global pollution:

var utils = (function() {
    var moduleA = {}
    moduleA.add = function(a, b) {
        console.log(a * b)
    }
    return moduleA
})()

utils.add(1,2)

However, manual dependency ordering is still required, as seen with libraries like jQuery that expose globals such as $.

Continuous Evolution of JavaScript Module Specifications

Increasing application complexity drove the need for more robust module systems.

CommonJS

In 2009, Mozilla engineer Kevin Dangoor created ServerJS, later renamed CommonJS, providing a universal API for module definition. Node.js adopted CommonJS for efficient server‑side module loading.

CommonJS conventions:

Each file is a module with its own scope.

Variables, functions, and classes are private to the file.

Modules expose interfaces via exports or module.exports.

Modules are loaded with require.

/* a.js */
function add(a, b) {
    return a + b;
}
module.exports = { add };

/* b.js */
const moduleA = require('./a.js');
const result = moduleA.add(5,10);
console.log(result);

CommonJS works well on the server with synchronous loading, but synchronous loading in browsers can cause blocking, prompting the need for asynchronous solutions.

AMD

AMD and CMD are design specifications, not implementations.

AMD (Asynchronous Module Definition) loads modules asynchronously, using define to declare modules and require to load them:

// Module definition
define(id?, dependencies?, factory);

// Module loading
require([module], callback);

AMD, together with RequireJS, provides a solution for client‑side module loading.

CMD

CMD (Common Module Definition), introduced by the SeaJS project, emphasizes near‑by dependencies and on‑demand loading, differing from AMD’s upfront dependency declaration.

// AMD example
define(['a','b'], function(a, b) {
    // modules a and b are executed and available here
});

// CMD example
define(function(require, exports) {
    var a = require('a'); // executed when needed
    if (false) {
        var b = require('b'); // never executed
    }
});

UMD

UMD (Universal Module Definition) bridges CommonJS and AMD, allowing a module to run in Node.js, browsers, or as a global variable:

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['dependency'], factory);
    } else if (typeof module === 'object' && module.exports) {
        // Node, CommonJS‑like
        module.exports = factory(require('dependency'));
    } else {
        // Browser globals (root is window)
        root.myModule = factory(root.dependency);
    }
}(this, function (dependency) {
    return {
        greet: function () {
            console.log('Hello, UMD');
        }
    };
}));

UMD decides whether to use AMD, CommonJS, or attach to the global object.

ES Module

Before ES Modules, JavaScript relied on third‑party specifications like CommonJS, AMD, and UMD. ES6 introduced native module support, offering static analysis of imports/exports.

ES6 modules are designed to be static, allowing compile‑time determination of dependencies and exported variables, unlike runtime‑only CommonJS and AMD.

Basic usage:

// a.js
export const add = (a, b) => {
    return a + b;
}

// main.js
import { add } from "./a.js";
add(1,2);

ES Modules unified module definition and usage, enhancing code reuse, maintainability, and enabling modern bundlers like Webpack and Rollup to optimize builds.

Conclusion

From the early IIFE pattern to the mature ES Module standard, JavaScript modular technology reflects the evolution of web development and signals a future of increasingly modular and component‑based applications. It also illustrates JavaScript’s transformation from a simple scripting language to a modern programming language capable of supporting complex applications.

References

Front‑end modular development history

JS modular development evolution

RequireJS and AMD specification

CommonJSAMDES modulesModule Systems
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.