深入理解 JavaScript 模块化
从 IIFE 到 ESM,模块化的完整演进史、循环依赖处理与打包原理
什么是 JavaScript 模块化?
定义:模块化是将程序拆分为独立、可复用的代码单元(模块)的编程方式。每个模块拥有自己的作用域,通过明确的 export/import 接口与其他模块通信,避免全局变量污染和命名冲突。
涉及场景:
- 大型项目架构:将数万行代码按功能拆分为可维护的独立文件
- 第三方库引入:
import lodash from 'lodash'等 npm 包管理 - 代码分割:
import()动态导入实现按需加载,减小首屏体积 - Tree Shaking:ESM 静态结构让打包工具自动移除未使用的代码
- SSR/ISR:Node.js 中 CommonJS 与 ESM 的互操作
- 微前端:不同子应用间的模块共享与隔离
作用:
- 作用域隔离:每个模块有独立作用域,不污染全局
- 依赖管理:明确声明模块间的依赖关系,避免加载顺序问题
- 代码复用:一个模块可以被多个模块引用,避免重复代码
- 团队协作:不同开发者独立开发不同模块,降低耦合
模块化演进时间线
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.14159CommonJS 的核心特点
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 APIImport 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 完整对比
| 特性 | CommonJS | ES Modules |
|---|---|---|
| 语法 | require / module.exports | import / export |
| 加载 | 运行时,同步 | 编译时,异步 |
| 值 | 拷贝 | 引用(实时绑定) |
| this | module.exports | undefined |
| 动态导入 | require(expr) | import(expr) |
| 顶层await | ❌ | ✅ |
| Tree Shaking | ❌ | ✅ |
| 循环依赖 | 返回未完成的导出对象 | 使用实时绑定(函数声明OK) |
| 文件扩展名 | .js(默认) | .mjs 或 package.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() 的使用场景 │
└──────────────────────────────────────────────────────────┘