引言:为什么模块化如此重要
在 JavaScript 诞生之初,它仅仅被设计为一种在浏览器中运行的小型脚本语言,用于处理表单验证和简单的 DOM 操作。然而,随着 Web 应用的复杂度呈指数级增长,代码量从几十行膨胀到数十万行,如何组织、隔离和复用代码成为了开发者面临的核心挑战。模块化,正是解决这一问题的关键思想。它让我们能够将庞大的代码库拆分为职责单一、高内聚低耦合的独立单元。回顾 JavaScript 模块化的演进史,就是一部开发者与语言局限不断抗争、并最终推动语言标准进化的历史。
第一阶段:全局污染与 IIFE 的救赎
在模块化概念普及之前,最原始的代码组织方式是将所有逻辑写在全局作用域中。这导致了著名的“全局变量污染”问题:不同脚本文件中的变量和函数极易发生命名冲突,依赖关系也只能靠 <script> 标签的加载顺序来隐式维护,脆弱且难以调试。
为了模拟私有作用域,开发者们发明了立即执行函数表达式。通过创建一个函数作用域,将变量和函数包裹其中并立即执行,外部无法直接访问内部实现,只暴露一个挂载在全局对象上的接口。这被称为“模块模式”。
var MyModule = (function() {
var privateVar = '内部变量';
function privateMethod() {
console.log(privateVar);
}
return {
publicMethod: function() {
privateMethod();
}
};
})();
IIFE 有效地解决了变量污染和部分依赖管理问题,但它依然没有解决脚本加载顺序和模块间依赖声明的标准化问题。每个模块仍然需要开发者手动管理全局命名空间。
第二阶段:服务端的探索——CommonJS
2009 年,Node.js 的诞生将 JavaScript 带到了服务器端。服务端环境没有 <script> 标签,文件直接从磁盘读取,因此需要一套全新的模块规范。CommonJS 应运而生,它成为了 Node.js 的默认模块系统。
CommonJS 的核心是 require 和 module.exports。每个文件就是一个模块,拥有独立的作用域。require 是同步加载,这在服务器端是可行的,因为文件读取速度快且无需考虑网络延迟。
// math.js
const add = (a, b) => a + b;
module.exports = { add };
// app.js
const { add } = require('./math.js');
console.log(add(1, 2));
CommonJS 的优点是简单直观,社区生态迅速繁荣,npm 仓库因此崛起。然而,它的同步加载特性在浏览器端是致命的——网络请求的延迟会导致页面长时间阻塞。因此,浏览器端需要一种异步的模块方案。
第三阶段:浏览器端的异步方案——AMD 与 RequireJS
为了解决浏览器端的异步加载问题,AMD 规范被提出,并由 RequireJS 等库实现。AMD 使用 define 函数来定义模块,并显式声明依赖数组,依赖加载完成后才执行回调。
define(['./math'], function(math) {
return {
calculate: function() {
return math.add(1, 2);
}
};
});
AMD 的优点是实现了真正的异步加载,适合浏览器环境。但它的语法相对冗长,且回调风格与 Node.js 的 CommonJS 风格差异较大,导致前后端代码难以复用。随后出现的 UMD 试图统一 CommonJS 和 AMD,让同一份代码既能在 Node.js 中运行,也能在浏览器中通过 AMD 加载,但本质上只是一种兼容性妥协,并未解决根本问题。
第四阶段:标准化的里程碑——ES Modules
随着前端工程化的爆发,社区迫切需要一个官方标准。2015 年,ECMAScript 2015 正式发布了 ES Modules,这是 JavaScript 语言层面的模块系统,标志着模块化终于有了“官方答案”。
ESM 使用 import 和 export 关键字,语法简洁清晰。与 CommonJS 不同,ESM 是静态的——模块的依赖关系在编译时就能确定,这使得 Tree Shaking 等优化成为可能。同时,ESM 的输出是值的引用,而 CommonJS 输出的是值的拷贝,这在处理循环依赖和动态更新时有本质区别。
// math.mjs
export const add = (a, b) => a + b;
export default function multiply(a, b) { return a * b; }
// app.mjs
import multiply, { add } from './math.mjs';
console.log(add(1, 2), multiply(3, 4));
在浏览器中,只需在 <script> 标签上添加 type="module" 即可启用 ESM。在 Node.js 中,可以通过 .mjs 扩展名或 package.json 中的 "type": "module" 来启用。ESM 还支持动态 import(),用于按需加载模块,兼顾了静态分析和懒加载的需求。
第五阶段:ESM 与 CommonJS 的共存与未来
尽管 ESM 是标准,但庞大的 npm 生态中仍有海量 CommonJS 包。Node.js 从 v12 开始逐步支持 ESM,并允许在 ESM 中通过 createRequire 或默认导入的方式使用 CommonJS 模块。而在 CommonJS 中加载 ESM 则必须使用动态 import(),因为 ESM 的异步特性无法被同步的 require 兼容。
如今,现代构建工具如 Vite、esbuild 和 Webpack 5 都已原生支持 ESM,并鼓励开发者迁移。浏览器端原生 ESM 的普及也使得开发阶段可以免打包运行,极大提升了开发体验。未来的趋势是明确的:ESM 是 JavaScript 模块化的最终形态,而 CommonJS 将作为历史遗产在 Node.js 生态中逐步过渡。
总结
从 IIFE 的模拟作用域,到 CommonJS 的服务端实践,再到 AMD 的浏览器异步方案,最终由 ESM 一统江湖,JavaScript 模块化的演进史是一部由需求驱动、由社区推动、由标准收束的进化史。理解这段历史,不仅能帮助我们更好地选择技术栈,更能深刻体会语言设计背后的权衡与智慧。今天,当我们轻松写下 import 语句时,不应忘记那些在全局污染和加载顺序中挣扎的前辈们。
未经允许不得转载:任鹏个人博客 » JavaScript 模块化演进史:从 IIFE 到 ESM 的完整历程

