Skip to content

深入理解 JavaScript 模块化

从 IIFE 到 ESM,模块化的完整演进史、循环依赖处理与打包原理

什么是 JavaScript 模块化?

定义:模块化是将程序拆分为独立、可复用的代码单元(模块)的编程方式。每个模块拥有自己的作用域,通过明确的 export/import 接口与其他模块通信,避免全局变量污染和命名冲突。

涉及场景

  • 大型项目架构:将数万行代码按功能拆分为可维护的独立文件
  • 第三方库引入import lodash from 'lodash' 等 npm 包管理
  • 代码分割import() 动态导入实现按需加载,减小首屏体积
  • Tree Shaking:ESM 静态结构让打包工具自动移除未使用的代码
  • SSR/ISR:Node.js 中 CommonJS 与 ESM 的互操作
  • 微前端:不同子应用间的模块共享与隔离

作用

  1. 作用域隔离:每个模块有独立作用域,不污染全局
  2. 依赖管理:明确声明模块间的依赖关系,避免加载顺序问题
  3. 代码复用:一个模块可以被多个模块引用,避免重复代码
  4. 团队协作:不同开发者独立开发不同模块,降低耦合

模块化演进时间线

1995  全局变量时代(命名冲突、污染全局)

2003  命名空间模式(对象封装)

2008  IIFE 模块模式(闭包隔离)

2009  CommonJS(Node.js,同步加载)

2011  AMD / RequireJS(浏览器,异步加载)

2011  CMD / SeaJS(延迟执行)

2014  UMD(兼容 CJS + AMD)

2015  ES Modules(语言级标准)

2018  动态 import()

2022  顶层 await

2025  import.meta.resolve / import attributes

阶段1:全局变量(原始时代)

javascript
// a.js
var name = '张三';
function greet() { return 'Hello ' + name; }

// b.js
var name = '李四'; // ❌ 覆盖了 a.js 的 name!
function greet() { return 'Hi ' + name; } // ❌ 覆盖了 a.js 的 greet!

// 问题:命名冲突、全局污染、依赖关系不明确

阶段2:命名空间模式

javascript
// 用对象组织代码,减少全局变量
var MyApp = MyApp || {};

MyApp.utils = {
  formatDate: function(date) { /* ... */ },
  parseUrl: function(url) { /* ... */ }
};

MyApp.models = {
  User: function(name) { this.name = name; }
};

// 使用
MyApp.utils.formatDate(new Date());

// 改善了命名冲突,但:
// 1. 所有属性都是公开的,没有私有变量
// 2. 依赖关系仍然不明确
// 3. 需要手动管理加载顺序

阶段3:IIFE 模块模式

javascript
// 利用闭包实现私有变量
var Module = (function() {
  // 私有变量
  var _count = 0;
  var _name = 'Module';

  // 私有方法
  function _log(msg) {
    console.log('[' + _name + '] ' + msg);
  }

  // 公开接口(Revealing Module Pattern)
  return {
    increment: function() {
      _count++;
      _log('count = ' + _count);
    },
    getCount: function() {
      return _count;
    }
  };
})();

Module.increment(); // [Module] count = 1
Module.getCount();  // 1
// Module._count    // undefined(私有)

// 依赖注入
var MyModule = (function($, _) {
  // 在模块内使用 jQuery 和 lodash
  return {
    init: function() {
      $('body').css('color', _.get(config, 'color', 'black'));
    }
  };
})(jQuery, lodash); // 将依赖作为参数传入

阶段4:CommonJS(2009,Node.js)

javascript
// math.js —— 导出
const PI = 3.14159;

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

function multiply(a, b) {
  return a * b;
}

module.exports = { PI, add, multiply };
// 或
exports.PI = PI;
exports.add = add;

// app.js —— 导入
const math = require('./math');
const { add, PI } = require('./math');

console.log(add(1, 2));  // 3
console.log(PI);          // 3.14159

CommonJS 的核心特点

javascript
// 1. 同步加载(适合服务端,不适合浏览器)
const fs = require('fs'); // 阻塞直到文件读取完成

// 2. 值的拷贝(不是引用)
// counter.js
let count = 0;
module.exports = {
  count,
  increment() { count++; }
};

// main.js
const counter = require('./counter');
counter.increment();
console.log(counter.count); // 0(拷贝,不会变!)
console.log(counter.increment); // 可以调用,但 count 不同步

// 3. 运行时加载(可以动态 require)
if (condition) {
  const moduleA = require('./a'); // ✅ 可以在条件中使用
}

// 4. 模块缓存(同一模块只执行一次)
require('./a'); // 第一次:执行模块代码
require('./a'); // 第二次:直接返回缓存

// 查看缓存
console.log(require.cache);

// 清除缓存(不推荐)
delete require.cache[require.resolve('./a')];

// 5. module.exports vs exports
// exports 是 module.exports 的引用
exports.foo = 'bar'; // ✅ 等价于 module.exports.foo = 'bar'
exports = { foo: 'bar' }; // ❌ 切断了引用,不会导出!
module.exports = { foo: 'bar' }; // ✅ 直接替换

CommonJS 循环依赖

javascript
// a.js
console.log('a 开始');
exports.done = false;
const b = require('./b'); // 加载 b
console.log('在 a 中, b.done =', b.done);
exports.done = true;
console.log('a 结束');

// b.js
console.log('b 开始');
exports.done = false;
const a = require('./a'); // 获取 a 的【未完成】的导出
console.log('在 b 中, a.done =', a.done); // false(a还没执行完)
exports.done = true;
console.log('b 结束');

// main.js
const a = require('./a');
const b = require('./b');

// 输出顺序:
// a 开始
// b 开始
// 在 b 中, a.done = false  ← a 的导出是未完成的快照
// b 结束
// 在 a 中, b.done = true
// a 结束

阶段5:AMD(2011,浏览器端)

javascript
// AMD (Asynchronous Module Definition)
// 代表实现:RequireJS

// 定义模块
define('math', ['dep1', 'dep2'], function(dep1, dep2) {
  // 依赖前置,提前执行
  return {
    add: function(a, b) { return a + b; }
  };
});

// 加载模块
require(['math'], function(math) {
  console.log(math.add(1, 2));
});

// 特点:
// 1. 异步加载(适合浏览器)
// 2. 依赖前置(define时声明所有依赖)
// 3. 提前执行依赖

阶段6:UMD(通用模块定义)

javascript
// UMD = CommonJS + AMD + 全局变量,兼容所有环境
(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['dep'], factory);
  } else if (typeof module === 'object' && module.exports) {
    // CommonJS
    module.exports = factory(require('dep'));
  } else {
    // 全局变量
    root.MyModule = factory(root.Dep);
  }
})(typeof self !== 'undefined' ? self : this, function(dep) {
  // 模块代码
  return { /* ... */ };
});

// 现在很少手写 UMD,构建工具(webpack/rollup)可以自动生成

阶段7:ES Modules(2015,语言标准)

javascript
// math.mjs —— 命名导出
export const PI = 3.14159;

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

export function multiply(a, b) {
  return a * b;
}

// math.mjs —— 默认导出
export default class Calculator {
  add(a, b) { return a + b; }
}

// app.mjs —— 导入
import Calculator from './math.mjs';           // 默认导入
import { add, PI } from './math.mjs';           // 命名导入
import { add as sum } from './math.mjs';        // 重命名
import * as math from './math.mjs';             // 命名空间导入
import Calculator, { add } from './math.mjs';   // 混合导入

// 重新导出(桶文件 barrel)
export { add, PI } from './math.mjs';
export { default } from './math.mjs';
export { add as sum } from './math.mjs';
export * from './math.mjs';                     // 导出所有命名导出
export * as math from './math.mjs';             // 命名空间重导出

ESM 核心特点

javascript
// 1. 编译时静态分析(import/export 必须在顶层)
import { add } from './math.mjs';

// ❌ 不能在条件中使用 import 声明
// if (condition) { import { add } from './math.mjs'; }

// ✅ 可以用动态 import()
if (condition) {
  const { add } = await import('./math.mjs');
}

// 2. 值的引用(实时绑定,Live Binding)
// counter.mjs
export let count = 0;
export function increment() { count++; }

// main.mjs
import { count, increment } from './counter.mjs';
console.log(count); // 0
increment();
console.log(count); // 1 ← 实时更新!(CJS 中是0)

// 3. 自动严格模式
// ESM 中不需要 'use strict',默认就是严格模式

// 4. this 是 undefined(不是全局对象)
console.log(this); // undefined(在ESM中)

// 5. 顶层 await(ES2022)
const data = await fetch('/api/config').then(r => r.json());
export const config = data;
// 其他模块 import 时会等待这个异步操作完成

ESM 循环依赖

javascript
// ESM 使用"实时绑定",处理循环依赖更优雅

// a.mjs
import { bar } from './b.mjs';
export function foo() {
  return 'foo + ' + bar();
}

// b.mjs
import { foo } from './a.mjs';
export function bar() {
  return 'bar';
}
// 调用 foo() 时 bar 已经定义好了
// ✅ 因为 ESM 的导出是引用,不是值的拷贝

// 但如果循环中有顶层执行代码,仍然会出问题:
// a.mjs
import { b } from './b.mjs';
console.log(b); // undefined(b.mjs 还没执行完)
export const a = 'a';

// b.mjs
import { a } from './a.mjs';
console.log(a); // undefined(a.mjs 还没执行完)
export const b = 'b';

import.meta

javascript
// import.meta 提供模块的元信息

// 当前模块的 URL
console.log(import.meta.url); // 'file:///Users/.../module.mjs'

// 解析相对路径(ES2025)
const workerUrl = import.meta.resolve('./worker.js');

// Node.js 中获取 __dirname 等价物
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Vite/Webpack 的特殊支持
import.meta.env.MODE;      // 'development' | 'production'
import.meta.env.BASE_URL;  // 基础路径
import.meta.hot;           // HMR API

Import Attributes(ES2025)

javascript
// 导入 JSON(需要 import attributes)
import data from './data.json' with { type: 'json' };

// 导入 CSS(构建工具支持)
import styles from './styles.css' with { type: 'css' };

// 动态导入也支持
const config = await import('./config.json', { with: { type: 'json' } });

CJS vs ESM 完整对比

特性CommonJSES Modules
语法require / module.exportsimport / export
加载运行时,同步编译时,异步
拷贝引用(实时绑定)
thismodule.exportsundefined
动态导入require(expr)import(expr)
顶层await
Tree Shaking
循环依赖返回未完成的导出对象使用实时绑定(函数声明OK)
文件扩展名.js(默认).mjspackage.json"type": "module"
条件导入require() 可在任何位置import 只能在顶层(import() 可动态)

打包工具如何处理模块?

javascript
// Webpack 将所有模块打包成一个 bundle
// 简化后的输出结构:
(function(modules) {
  var cache = {};

  function __webpack_require__(moduleId) {
    if (cache[moduleId]) return cache[moduleId].exports;

    var module = cache[moduleId] = { exports: {} };
    modules[moduleId](module, module.exports, __webpack_require__);
    return module.exports;
  }

  __webpack_require__(0); // 入口模块
})([
  /* 0 */ function(module, exports, require) {
    var math = require(1);
    console.log(math.add(1, 2));
  },
  /* 1 */ function(module, exports) {
    exports.add = function(a, b) { return a + b; };
  }
]);

// Tree Shaking 原理(基于ESM的静态分析)
// 1. 构建工具分析 import/export,构建依赖图
// 2. 标记所有被使用的导出(Mark)
// 3. 移除未被使用的导出(Sweep)
// 4. 需要 ESM 的静态结构(CJS 的 require 是动态的,无法分析)

// 影响 Tree Shaking 的因素
// ❌ 副作用代码(模块顶层的 console.log、全局修改等)
// ❌ 动态属性访问 obj[key]
// ✅ package.json "sideEffects": false 声明无副作用
// ✅ 纯函数和常量导出

总结

模块化核心知识点:
┌──────────────────────────────────────────────────────────┐
│ 演进路线                                                  │
│ 全局变量 → 命名空间 → IIFE → CJS → AMD → UMD → ESM       │
│                                                          │
│ CommonJS                                                  │
│ • 同步加载、值的拷贝、运行时、Node.js 标准                   │
│ • require/module.exports                                  │
│                                                          │
│ ES Modules                                                │
│ • 异步加载、值的引用(实时绑定)、编译时、语言标准             │
│ • import/export、支持 Tree Shaking                        │
│ • 顶层 await、import.meta、Import Attributes              │
│                                                          │
│ 面试重点                                                   │
│ • CJS vs ESM 的核心区别(值拷贝 vs 引用)                   │
│ • 循环依赖的处理差异                                        │
│ • Tree Shaking 为什么需要 ESM                              │
│ • 动态 import() 的使用场景                                  │
└──────────────────────────────────────────────────────────┘