Evolution of JS Modules

Med

JavaScript started with no module system. Over 20 years, we went from global scripts to ES Modules. Understanding this history explains why you see require(), define(), and import in different codebases.

Interactive Visualization

1

Global Scripts

1995-2009

Everything lived in the global scope. Scripts had to be loaded in the right order.

<script>Global vars
Example Code
<!-- index.html - Load scripts in order! -->
<script src="jquery.js"></script>
<script src="utils.js"></script>
<script src="app.js"></script>

// utils.js - Everything is global
var Utils = {};
Utils.formatDate = function(date) {
  return date.toISOString();
};

// app.js - Hope Utils loaded first!
var result = Utils.formatDate(new Date());

// Problem: What if utils.js loads after app.js?
// Problem: What if another lib also uses "Utils"?
Problems Solved
  • Simple to understand
  • No build step needed
  • Works in any browser
New Challenges
  • Global namespace pollution
  • Order-dependent loading
  • Name collisions between libraries
These challenges led to: IIFE Pattern
1 / 6
Key Insight: ES Modules won because static imports enable tree-shaking and native browser support.

Key Points

  • Global Scripts (1995): No modules, everything global, order-dependent
  • IIFE Pattern (2009): Closures for encapsulation, still manual ordering
  • CommonJS (2009): Node.js require/exports, synchronous, server-focused
  • AMD (2011): Async loading for browsers, verbose callback syntax
  • UMD (2013): Universal wrapper for AMD + CommonJS + global
  • ES Modules (2015): Official standard, static imports, tree-shaking

Code Examples

Era 1: Global Scripts (1995-2009)

<!-- Everything global, order matters! -->
<script src="jquery.js"></script>
<script src="utils.js"></script>
<script src="app.js"></script>

// utils.js
var Utils = {};
Utils.formatDate = function(d) { return d.toISOString(); };

// app.js - Hope Utils loaded first!
console.log(Utils.formatDate(new Date()));

// Problem: Name collisions, load order, no encapsulation

Every script shared the global scope. Load order mattered, and name collisions were common.

Era 2: IIFE Pattern (2009-2012)

// Module Pattern using IIFE + Closure
var MyModule = (function() {
  // Private (hidden in closure)
  var counter = 0;

  // Public API
  return {
    increment: function() { counter++; },
    getCount: function() { return counter; }
  };
})();

MyModule.increment();  // Works
MyModule.counter;      // undefined (private!)

IIFEs created private scope via closures, but you still had one global per module.

Era 3: CommonJS (Node.js)

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

// app.js - Import
const { add } = require('./math');
console.log(add(2, 3)); // 5

// Synchronous: require() blocks until file loads
// Designed for servers with fast file system access

Node.js introduced CommonJS - the first real module system. Synchronous loading worked for servers.

Era 4: AMD (Browser Async)

// RequireJS - async loading for browsers
define('app', ['jquery', 'utils'], function($, utils) {
  // Dependencies load asynchronously
  // Then this callback executes
  return {
    init: function() {
      $('#app').text(utils.formatDate());
    }
  };
});

// Verbose, but enabled lazy loading

AMD solved async loading for browsers, but the callback syntax was verbose.

Era 5: UMD (Universal)

// UMD: Works in AMD, CommonJS, AND browsers
(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(['dep'], factory);           // AMD
  } else if (typeof exports === 'object') {
    module.exports = factory(require('dep')); // CJS
  } else {
    root.MyLib = factory(root.Dep);     // Global
  }
}(this, function(dep) {
  return { /* module */ };
}));

UMD wrapped modules to work everywhere. Complex but necessary for library authors.

Era 6: ES Modules (The Standard)

// math.js - Named exports
export function add(a, b) { return a + b; }
export const PI = 3.14;

// Default export
export default class Calculator {}

// app.js - Import
import Calculator, { add, PI } from './math.js';

// Dynamic import (code splitting!)
const heavy = await import('./heavy.js');

// Static analysis enables tree-shaking!

ES Modules are the official standard - static, tree-shakeable, and natively supported.

Common Mistakes

  • Mixing require() and import in the same file (different systems!)
  • Forgetting .js extension in browser ES modules
  • Not understanding "type": "module" in package.json
  • Thinking require() works in browsers without a bundler

Interview Tips

  • Explain why CommonJS is synchronous (designed for Node file system)
  • Know why ES Modules enable tree-shaking (static analysis)
  • Understand the dual package hazard when publishing libraries
  • Be ready to explain require.cache and module resolution
  • Know the difference: CJS copies values, ESM binds live references