如果你刚接触现代前端开发,可能会觉得 import 和 export 是理所当然的存在。但在 JavaScript 诞生的头十年里,根本没有“模块”这个概念。这段历史不仅是技术的演进,更是一次次对代码组织方式的深刻反思。理解它,才能真正明白现代模块化方案的设计初衷。
第一阶段:无模块化的“蛮荒时代”
早期的网页 JavaScript 代码量很小,通常就几十到几百行,负责一些简单的表单验证、弹窗提示或图片轮播。那时的组织方式极其原始:
<!-- 按照加载顺序依次执行 -->
<script src="jquery.js"></script>
<script src="utils.js"></script>
<script src="app.js"></script>
没有模块概念,所有变量和函数都直接暴露在全局作用域下,多个脚本文件的“沟通”方式是直接在 window 上挂载属性。这种模式带来的问题随着代码量增长迅速爆发:
- 全局作用域污染:两个文件如果定义了同名变量或函数,后加载的会悄悄覆盖先前的,导致难以排查的 bug。
- 依赖关系隐式且脆弱:必须由开发者自己保证脚本加载顺序正确,
app.js依赖utils.js,一旦顺序出错,运行失败,而且没有任何错误提示能指明“依赖缺失”。 - 代码组织混乱:大型项目的所有逻辑散落在一堆文件中,函数满天飞,维护成本极高。
这是一个“约定全靠喊”的时期,团队协作必须遵守严格的手工规范,稍有不慎就踩坑。
第二阶段:立即执行函数(IIFE)的“作用域隔离”
为了解决全局作用域污染,开发者们开始利用 JavaScript 的函数作用域特性,想出了一个精妙的模式:立即执行函数表达式(Immediately Invoked Function Expression,IIFE)。
// utils.js
var Utils = (function() {
// 私有变量,外部无法直接访问
var privateData = 'secret';
// 暴露给外部的 API
return {
doSomething: function() {
console.log('doing something with ' + privateData);
}
};
})();
// app.js
Utils.doSomething(); // 可以调用
console.log(privateData); // undefined,私有变量被隔离
IIFE 的核心思路:
- 创建一个匿名函数并立即执行,该函数内部声明的变量都封闭在函数作用域内。
- 通过返回一个对象或向
window挂载命名空间,对外暴露必要的接口。
这种模式有效地解决了全局变量互相覆盖的问题,同时实现了“私有成员”——外部无法直接访问函数内部的变量,只有被返回的公有方法能操作它们。jQuery 等早期主流库都采用了这一模式。
IIFE 的局限也很明显:
- 依然依赖全局变量传递模块(如上面的
Utils),会污染window对象。 - 依赖管理依然靠手动确保加载顺序,没有明确的依赖声明。
- 模块越多,产生的全局变量越多,命名冲突风险又渐渐重现。
它是过渡方案,但很有价值——证明了作用域隔离和暴露接口是模块化的合理方向,为真正的模块化规范铺平了道路。
第三阶段:规范标准化的“百家争鸣到统一”
随着 Node.js 在 2009 年诞生,服务端 JavaScript 遇到了更紧迫的需求:必须要有一套能在不同文件间方便引用、隔离且可管理依赖的模块系统。于是,CommonJS 规范迅速成为 Node.js 的内置模块化方案。与此同时,浏览器端因为没有内置模块加载能力,社区先后出现了 AMD 和 CMD 规范,通过第三方加载器来实现异步模块化。再到 2015 年,ECMAScript 标准正式引入 ES Modules,终于实现了浏览器与 Node.js 的统一模块化方案。
这个阶段的核心进步在于:
- 从此有了明确的
require/export/import等关键字和语法。 - 依赖关系从隐式变成了显式声明,工具可以自动分析和处理。
- 每个模块拥有独立作用域,不再需要手动 IIFE,模块之间的变量彻底隔离。
回顾整个演进脉络,可以看到一条清晰的路线:全局污染 → 作用域隔离 → 显式依赖 → 标准化统一。这不仅是语法的变化,更是前端工程化思想的成熟——从靠开发者自律,到靠工具和标准保障。后续章节将详细拆解 CommonJS、AMD、CMD 以及 ES Modules 的具体机制和差异。