模块化介绍
一、模块化的核心定义
模块化是将复杂的程序代码,按照功能、逻辑或职责拆分成多个独立、可复用、低耦合的"模块(Module)" 的编程思想和实践方式。
每个模块就像一个"功能组件":
- 内部封装了专属的逻辑和数据(仅对外暴露需要被使用的部分);
- 模块间通过标准化的"导入(import)/导出(export)"接口 通信;
- 最终通过组合这些模块,构建出完整的应用。
简单来说:模块化的核心是「分而治之」------把大问题拆成小问题,让代码从"一锅粥"变成"按功能分装的小盒子"。
二、为什么需要模块化?(解决传统代码的痛点)
在模块化规范出现前,前端/后端代码都面临严重问题,以浏览器端 JavaScript 为例:
1. 非模块化的痛点(早期原生 JS 写法)
html
<!-- 传统写法:所有代码堆在全局作用域 -->
<script>
// 登录功能
let username = "";
function login() { /* ... */ }
// 支付功能
let username = "guest"; // 全局变量冲突!覆盖了上面的 username
function pay() { /* ... */ }
</script>
<script src="utils.js"></script> <!-- 外部文件也污染全局,无法确定依赖顺序 -->
核心问题:
- 全局变量污染:所有变量/函数都挂在
window上,极易重名冲突; - 依赖混乱:代码执行顺序完全靠
<script>标签顺序,维护时改一个标签位置就可能崩; - 复用性差:功能代码无法便捷复用,只能复制粘贴;
- 维护成本高:代码无边界,改一处逻辑可能影响全局。
2. 模块化的解决思路
把上面的代码拆成两个独立模块,仅暴露必要接口:
javascript
// login.module.js(登录模块)
let _username = ""; // 私有变量,外部不可见(封装性)
export function login(name) { // 导出需要对外的功能
_username = name;
console.log(`登录:${_username}`);
}
// pay.module.js(支付模块)
import { login } from "./login.module.js"; // 导入依赖模块
let _payStatus = "ready"; // 私有变量
export function pay(amount) {
login("user1"); // 复用登录功能
console.log(`支付 ${amount} 元`);
}
核心优势:
- 无全局污染:模块内的变量默认私有,仅导出的部分可被外部访问;
- 依赖清晰:明确声明"我依赖哪个模块",无需靠标签顺序;
- 复用性强:一个模块可被任意其他模块导入使用;
- 易维护:每个模块只负责一个功能,改登录逻辑只需动
login.module.js,不影响支付模块。
三、模块化的核心特征
不管是 CommonJS 还是 ESM,所有模块化规范都遵循以下核心特征:
| 特征 | 说明 |
|---|---|
| 封装性 | 模块内部的代码(如私有变量、辅助函数)默认对外不可见,仅暴露指定接口; |
| 独立性 | 模块间低耦合(改一个模块的内部逻辑,只要接口不变,其他模块无需修改); |
| 复用性 | 一个模块可被多个地方导入,避免重复写相同逻辑; |
| 可组合性 | 通过导入/导出组合多个模块,拼接出完整功能; |
| 标准化 | 有统一的语法规范(如 ESM 的 export/import,CommonJS 的 require),保证不同模块能协作。 |
四、模块化 ≠ 某一种规范(衔接之前的内容)
你之前问的 CommonJS 和 ESM,并不是"模块化本身",而是模块化的两种"实现规范" ------ 就像"做蛋糕"是核心目标(模块化),CommonJS 和 ESM 是两种"做蛋糕的配方(规范)",配方不同(加载机制、语法、值传递),但最终目的都是做出"蛋糕(模块化代码)"。
| 层面 | 说明 |
|---|---|
| 模块化思想 | 分而治之、封装、复用(核心); |
| 模块化规范 | CommonJS/ESM/AMD/CMD 等(实现模块化的"规则"); |
| 模块化工具 | Webpack/Vite/Rollup(兼容不同规范,让模块化代码能在浏览器/Node.js 运行)。 |
五、一句话总结
模块化就是给代码"分盒子、贴标签":把不同功能的代码放进不同的"盒子(模块)",盒子只对外暴露"接口标签(导出)",其他模块通过"标签(导入)"使用这个盒子的功能,最终让代码从混乱的"全局大锅饭"变成有序的"组件化拼装"。
主流模块化规范
一、前言:模块化规范的核心定位
模块化的核心目标是分治、封装、复用、解耦,而不同模块化规范的诞生,本质是为了适配不同运行环境(服务端/浏览器)、不同加载需求(同步/异步)的产物。
主流模块化规范可分为「核心规范」和「兼容方案」两类:
- 核心规范:CommonJS(服务端)、AMD(浏览器异步)、CMD(浏览器按需)、ESM(ES6 通用标准化);
- 兼容方案:UMD(适配多环境的通用封装)。
以下是各规范的详细解析,以及横向对比和使用建议。
二、主流模块化规范详解(定位+设计背景+核心特征)
1. CommonJS
定位
Node.js 原生默认的服务端模块化规范,适配「服务端文件系统」的模块化需求。
设计背景
2009 年 Node.js 诞生时,JavaScript 尚无官方模块化规范,而服务端编程天然需要模块化(如文件操作、进程管理)。由于服务端文件读取速度极快,同步加载 不会造成性能问题,因此 CommonJS 设计为「同步加载模块」,优先保证语法简单、贴合服务端开发习惯。
核心特征
- 加载方式:同步加载(执行到
require()时才加载并执行模块); - 核心语法:
module.exports/exports导出,require()导入; - 执行时机:模块加载时立即执行(运行时加载);
- 值传递:值的拷贝(基本类型拷贝、引用类型浅引用);
- 典型环境:Node.js(浏览器不原生支持,需打包工具转换)。
2. AMD(Asynchronous Module Definition,异步模块定义)
定位
浏览器端「异步加载」的模块化规范,代表实现:RequireJS。
设计背景
早期浏览器中,JS 模块通过 <script> 标签同步加载会阻塞 DOM 渲染和页面交互,且网络请求异步特性与同步加载冲突。AMD 专为浏览器设计,核心解决「异步加载模块+明确依赖顺序」问题。
核心特征
- 加载方式:异步加载(模块依赖先加载,加载完成后执行回调);
- 核心语法:
define([依赖列表], factory)定义模块,require([模块路径], callback)加载模块; - 执行时机:依赖全部加载完成后,立即执行 factory 函数(「预加载+提前执行」);
- 典型场景:早期前端模块化项目(如 jQuery 时代的复杂页面)。
javascript
// AMD 示例(RequireJS)
// 定义模块(依赖 jQuery)
define(['jquery'], function($) {
return {
show: function() {
$('body').html('AMD 模块执行');
}
};
});
// 加载模块
require(['./module'], function(mod) {
mod.show();
});
3. CMD(Common Module Definition,通用模块定义)
定位
浏览器端「按需加载」的模块化规范,代表实现:SeaJS(阿里玉伯开发,已停止维护)。
设计背景
AMD 语法冗余(需提前声明所有依赖),且「提前执行」不符合 CommonJS 的书写习惯。CMD 试图兼容 CommonJS 语法,同时适配浏览器异步场景,核心是「按需加载、就近执行」。
核心特征
- 加载方式:异步加载,但「按需执行」(依赖在用到时才加载);
- 核心语法:
define(function(require, exports, module) {}),通过require()就近导入依赖; - 执行时机:模块定义时不立即加载依赖,执行到
require()时才加载并执行依赖(「懒加载+就近执行」); - 典型场景:早期国内前端项目(SeaJS 生态),现已被 ESM 替代。
javascript
// CMD 示例(SeaJS)
define(function(require, exports, module) {
// 就近加载依赖(用到时才加载)
const $ = require('jquery');
exports.show = function() {
$('body').html('CMD 模块执行');
};
});
// 加载模块
seajs.use('./module', function(mod) {
mod.show();
});
4. ESM(ECMAScript Module,ES6 模块)
定位
ES6 官方标准化的「通用模块化规范」,适配浏览器+Node.js 双环境。
设计背景
此前模块化规范碎片化(服务端 CommonJS、浏览器 AMD/CMD),ES6 试图统一前后端模块化标准,同时引入「静态编译」特性,支持 Tree-Shaking 等现代优化。
核心特征
- 加载方式:浏览器端异步加载(
<script type="module">)、Node.js 支持同步/异步,核心是「编译时静态加载」; - 核心语法:
export/export default导出,import/import()导入; - 执行时机:编译时解析导入导出,运行时执行模块(静态分析+动态绑定);
- 值传递:动态引用(导出值修改,导入端同步更新);
- 典型环境:现代浏览器(原生支持)、Node.js 14.13+(配置
"type": "module")。
5. UMD(Universal Module Definition,通用模块定义)
定位
「跨环境兼容方案」(非独立规范),适配 CommonJS/AMD/全局变量三种场景。
设计背景
第三方库(如 Lodash、Vue 早期版本)需要同时支持 Node.js(CommonJS)、浏览器(AMD/全局变量),因此 UMD 作为「兼容层」封装模块,让同一套代码在多环境下可用。
核心特征
- 实现逻辑:通过判断环境变量(
typeof module !== 'undefined'检测 CommonJS,typeof define === 'function'检测 AMD),自动适配对应规范; - 典型场景:通用类库开发(需兼容多环境)。
javascript
// UMD 简化示例
(function(root, factory) {
if (typeof module === 'object' && module.exports) {
// CommonJS 环境(Node.js)
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
// AMD 环境(RequireJS)
define([], factory);
} else {
// 浏览器全局变量
root.UMDModule = factory();
}
})(this, function() {
return { name: 'UMD 模块' };
});
三、核心差异对比(横向维度)
| 规范 | 核心定位 | 加载方式 | 执行时机 | 核心语法 | 设计目标 | 典型环境 |
|---|---|---|---|---|---|---|
| CommonJS | 服务端模块化 | 同步加载 | 运行时加载(执行到即加载) | module.exports/require |
适配 Node.js 服务端同步需求 | Node.js |
| AMD | 浏览器异步模块化 | 异步加载 | 预加载依赖,全部加载后执行 | define([依赖], factory) |
解决浏览器同步加载阻塞问题 | 浏览器(RequireJS) |
| CMD | 浏览器按需模块化 | 异步加载(懒) | 就近加载,执行到才加载 | define(require, exports) |
简化 AMD 语法,贴近 CommonJS | 浏览器(SeaJS) |
| ESM | 通用标准化模块化 | 静态编译时加载 | 编译时解析,运行时执行 | export/import |
统一前后端模块化,支持优化 | 浏览器+Node.js |
| UMD | 跨环境兼容方案 | 适配目标环境 | 适配目标环境 | 无独立语法(封装其他规范) | 模块跨环境复用 | 全环境 |
四、其他关键差异
1. 值传递机制
- CommonJS:值的拷贝(基本类型拷贝,引用类型浅引用);
- AMD/CMD:类似 CommonJS(值拷贝);
- ESM:动态引用(导出值修改,导入端同步更新);
- UMD:适配目标规范的传递机制(如适配 CommonJS 则拷贝,适配 ESM 则绑定)。
2. 循环依赖处理
- CommonJS:返回「未执行完的模块导出对象」(可能拿到不完整值);
- AMD:提前加载所有依赖,循环依赖时返回已定义的接口(比 CommonJS 更可控);
- CMD:按需加载,循环依赖时需手动处理(易出问题,生态弱);
- ESM:动态绑定,循环依赖时仍能同步后续修改(最完善)。
3. 静态分析/优化支持
- CommonJS/AMD/CMD:运行时加载,无法做静态分析(不支持 Tree-Shaking);
- ESM:编译时静态加载,支持 Tree-Shaking、路径校验、类型检查(现代构建工具核心依赖);
- UMD:依赖目标规范,适配 ESM 则支持优化,否则不支持。
4. 生态与维护状态
- CommonJS:Node.js 核心生态,长期维护(但逐步被 ESM 替代);
- AMD:RequireJS 仍维护,但生态萎缩(被 ESM 替代);
- CMD:SeaJS 已停止维护,基本淘汰;
- ESM:ES 官方标准,现代前端/Node.js 核心生态,持续迭代;
- UMD:作为库兼容方案,仍广泛使用(如老版本第三方库)。
五、所有规范的相同点
无论哪种模块化规范,核心目标和底层逻辑完全一致:
- 封装性:模块内部变量/函数默认私有,仅对外暴露指定接口;
- 解耦性:模块间低耦合,修改内部逻辑不影响外部(只要接口不变);
- 复用性:模块可被任意其他模块导入,避免重复代码;
- 依赖管理:明确声明模块依赖,解决传统全局代码的依赖混乱问题;
- 无全局污染 :模块内变量不挂载到全局作用域(如
window/global),避免命名冲突。
六、使用建议(分场景)
1. Node.js 项目
- 新项目:优先使用 ESM(配置
package.json中"type": "module"),兼容 Tree-Shaking、静态分析等现代特性; - 老项目:可混合使用 CommonJS(存量代码)+ ESM(新代码),Node.js 原生支持双向兼容;
- 特殊场景(动态加载):用 CommonJS 的
require()或 ESM 的import()函数。
2. 浏览器项目
- 现代项目(无兼容老浏览器需求):直接使用 ESM(
<script type="module">),或通过 Vite/Webpack 打包(自动优化); - 老项目(需兼容 IE):用 Webpack 打包 ESM 为兼容代码,或临时用 RequireJS(AMD)兜底(不推荐新开发);
- 完全淘汰 CMD(SeaJS):生态停更,无维护保障。
3. 通用类库开发(需兼容多环境)
- 核心代码用 ESM 编写,通过打包工具(Rollup/Webpack)输出 UMD 格式(兼容 CommonJS/AMD/全局变量);
- 同时输出 ESM 格式(供现代项目直接导入)和 CommonJS 格式(供 Node.js 老项目使用)。
4. 性能优化优先的场景
- 必须使用 ESM:只有 ESM 支持 Tree-Shaking、静态路径分析等优化,可大幅减小打包体积;
- 避免 AMD/CMD:无优化能力,仅适用于老项目兼容。
七、总结
模块化规范的演进本质是「从环境专用到通用标准化」:
- 早期:服务端用 CommonJS,浏览器用 AMD/CMD(碎片化);
- 现在:ESM 成为统一标准(前后端通用),UMD 作为跨环境兼容方案补充;
- 未来:ESM 将完全替代 CommonJS/AMD/CMD,成为 JavaScript 模块化的唯一核心规范。
开发时无需纠结所有规范,核心掌握「ESM(主流)+ CommonJS(兼容老 Node 代码)+ UMD(库开发)」即可覆盖 99% 的场景。
ES6 模块(ESM,ECMAScript Module) 和 CommonJS 模块 详细对比
一、核心定位与设计背景
| 特性 | CommonJS | ES6 模块 (ESM) |
|---|---|---|
| 设计目标 | 服务端(Node.js)模块化,同步加载 | 通用模块化(浏览器+Node.js),支持异步 |
| 原生支持环境 | Node.js 原生支持,浏览器不支持 | 现代浏览器原生支持,Node.js 14.13+ 支持(需配置) |
| 加载阶段 | 运行时加载(动态) | 编译时加载(静态) |
二、核心语法差异
1. 导出语法
-
CommonJS :通过
module.exports(默认导出)或exports(命名导出),本质是导出一个「对象」。javascript// 命名导出 exports.foo = 1; exports.bar = () => {}; // 默认导出(覆盖 exports 指向) module.exports = { name: "commonjs" };⚠️ 注意:
exports是module.exports的引用,直接赋值exports = {}会失效(需用module.exports)。 -
ESM :通过
export(命名导出)或export default(默认导出),语法更严格。javascript// 命名导出 export const foo = 1; export function bar() {}; // 默认导出(只能有一个) export default { name: "esm" };
2. 导入语法
-
CommonJS :通过
require()导入,支持动态路径、条件导入(运行时特性)。javascript// 整体导入 const mod = require("./module.js"); // 解构导入(本质是对导出对象解构) const { foo } = require("./module.js"); // 动态导入(运行时判断) if (true) { const mod = require("./dynamic.js"); // 合法 } -
ESM :通过
import导入,静态语法(编译时确定),默认不支持动态路径(需用import()函数)。javascript// 命名导入(需与导出名一致) import { foo, bar } from "./module.js"; // 默认导入(自定义名称) import mod from "./module.js"; // 动态导入(返回 Promise,兼容运行时动态加载) if (true) { import("./dynamic.js").then(mod => {}); // 合法 }⚠️ 注意:ESM 静态导入要求路径必须是「字面量」,
import(./${path}.js)仅能通过动态import()实现。
三、核心机制差异(最关键)
1. 加载时机:静态 vs 动态
-
CommonJS:运行时加载
require()在代码执行到这一行时才会加载模块,模块代码也在此时执行;优点:支持动态逻辑(如
require(__dirname + "/mod.js"));缺点:无法做「静态分析」(如 Tree-Shaking 优化)。
-
ESM:编译时加载
导入/导出语句会在代码「解析阶段」(编译时)被处理,而非执行阶段;
优点:
- 支持 Tree-Shaking(剔除未使用的导出,减小打包体积);
- 编辑器可提前做类型检查、路径校验;
缺点:静态语法限制(默认不支持条件导入,需用import()兜底)。
2. 值传递:拷贝 vs 动态引用
这是二者最核心的差异之一,直接影响模块间的数据同步:
-
CommonJS:值的拷贝(浅拷贝)
导入的是模块导出值的「拷贝」:
- 基本类型(如数字、字符串):导入后模块内部修改,外部不会同步;
- 引用类型(如对象、函数):拷贝的是引用地址,内部修改属性会同步,但重新赋值不会。
javascript// mod.js (CommonJS) let num = 1; let obj = { a: 1 }; exports.num = num; exports.obj = obj; exports.update = () => { num = 2; // 基本类型重新赋值 obj.a = 2; // 引用类型修改属性 }; // main.js const { num, obj, update } = require("./mod.js"); console.log(num); // 1 console.log(obj.a); // 1 update(); console.log(num); // 1(基本类型拷贝,不同步) console.log(obj.a); // 2(引用类型属性同步) -
ESM:动态引用(绑定)
导入的是模块导出值的「实时绑定」,模块内部修改值,外部导入的变量会同步更新(因为 ESM 导出的是「变量引用」,而非值)。
javascript// mod.js (ESM) let num = 1; let obj = { a: 1 }; export { num, obj }; export function update() { num = 2; obj.a = 2; } // main.js import { num, obj, update } from "./mod.js"; console.log(num); // 1 console.log(obj.a); // 1 update(); console.log(num); // 2(动态引用,同步更新) console.log(obj.a); // 2
3. 循环依赖处理
循环依赖(如 A 导入 B,B 导入 A)是模块化中常见场景,二者处理逻辑不同:
-
CommonJS :返回「已执行的部分导出」,可能拿到不完整的模块。
模块加载时会生成一个「导出对象」,即使模块未执行完,也会先把当前的导出对象返回给依赖方,后续修改仅对引用类型生效。
javascript// a.js const b = require("./b.js"); console.log("a 拿到的 b:", b); // { foo: undefined }(b 未执行完) module.exports = { bar: 1 }; // b.js const a = require("./a.js"); console.log("b 拿到的 a:", a); // {}(a 未执行完) module.exports = { foo: 2 }; // main.js require("./a.js"); // 输出: // b 拿到的 a: {} // a 拿到的 b: { foo: undefined } -
ESM :通过「动态绑定」处理,即使循环依赖,也能同步后续修改。
因为 ESM 导入的是「引用」而非拷贝,模块未执行完时,导入的变量会指向「未完成的绑定」,后续模块执行完毕后,值会自动同步。
javascript// a.js (ESM) import { foo } from "./b.js"; console.log("a 拿到的 foo:", foo); // undefined(b 未执行完) export const bar = 1; // b.js (ESM) import { bar } from "./a.js"; console.log("b 拿到的 bar:", bar); // undefined(a 未执行完) export let foo = 2; // 后续修改 foo setTimeout(() => { foo = 3; }, 0); // main.js import { foo } from "./b.js"; console.log("main 拿到的 foo:", foo); // 2 setTimeout(() => { console.log(foo); }, 100); // 3(动态绑定同步)
四、其他关键差异
1. this 指向
-
CommonJS :模块内的
this指向module.exports(模块导出对象);javascript// commonjs.js console.log(this === module.exports); // true -
ESM :模块默认启用严格模式,
this指向undefined;javascript// esm.js console.log(this); // undefined
2. 顶层变量
-
CommonJS :自带
module、exports、require、__filename、__dirname等顶层变量; -
ESM :无上述变量,Node.js 中需通过
import.meta.url模拟__filename/__dirname:javascript// ESM 中模拟 __dirname import { fileURLToPath } from "url"; import { dirname } from "path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename);
3. 加载方式
- CommonJS:同步加载(Node.js 服务端文件读取快,同步无性能问题;浏览器端同步加载会阻塞渲染,需打包工具转换);
- ESM :浏览器端默认异步加载(通过
<script type="module">加载,不阻塞 DOM 渲染),Node.js 中也支持异步加载。
五、总结:核心差异表
| 对比维度 | CommonJS | ESM |
|---|---|---|
| 加载阶段 | 运行时(动态) | 编译时(静态) |
| 值传递 | 拷贝(基本类型)/浅引用 | 动态绑定(实时引用) |
| 语法 | module.exports/require | export/import |
| 循环依赖 | 返回不完整拷贝 | 动态绑定同步更新 |
| this 指向 | module.exports | undefined |
| 顶层变量 | 有 __filename/__dirname | 无,需手动模拟 |
| Tree-Shaking | 不支持(动态加载) | 支持(静态分析) |
| 动态导入 | 原生支持(require) | 需 import() 函数 |
六、使用建议
- Node.js 项目 :
- 新项目优先用 ESM(配置
package.json中"type": "module"),兼容现代特性; - 老项目若依赖大量 CommonJS 模块,可混合使用(Node.js 支持
require导入 ESM,或import导入 CommonJS)。
- 新项目优先用 ESM(配置
- 浏览器项目 :
- 直接用 ESM(
<script type="module">),或通过 Vite/Webpack 打包(自动处理兼容); - 无需兼容 CommonJS(浏览器无原生支持)。
- 直接用 ESM(
- 性能优化 :
- 需 Tree-Shaking 减包体积 → 必须用 ESM;
- 服务端动态加载/条件导入 → CommonJS 更便捷(或 ESM 的
import())。