JavaScript 模块化:CommonJS、AMD、ES6 模块
随着前端项目规模不断扩大,代码复用和工程化成为必然需求。模块化应运而生,它将复杂代码拆分为独立文件,每个文件就是一个模块,拥有自己的作用域。本文将深入讲解 JavaScript 发展历程中的三种主流模块规范:CommonJS、AMD 和 ES6 模块,帮助你理解它们的区别并做出正确选择。
1. 为什么需要模块化
在没有模块化的年代,开发者通过 <script> 标签逐一引入文件。这种方式存在诸多痛点:全局变量污染严重,文件依赖顺序难以维护,代码复用率低下。例如,一个大型页面可能需要引入十几个 script 标签,顺序错了就会报错,调试极为困难。
模块化的核心价值在于解决这些问题。它通过封装实现信息隐藏,每个模块暴露特定接口供其他模块调用,开发者只需关心模块间的契约而非内部实现。
2. CommonJS 规范
2.1 诞生背景
CommonJS 规范由 Kevin Dangoor 等人于 2009 年提出,最初名为 ServerJS,旨在为浏览器外的 JavaScript 运行环境制定标准。Node.js 采纳了这一规范,使其成为服务端 JavaScript 的事实标准。
2.2 核心语法
CommonJS 使用 module.exports 导出模块,使用 require 同步加载模块。
// math.js
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = {
add,
multiply
};
// main.js
const { add, multiply } = require('./math');
console.log(add(2, 3)); // 输出: 5
console.log(multiply(4, 5)); // 输出: 20
你也可以逐个导出:
exports.add = function(a, b) {
return a + b;
};
注意:exports 是 module.exports 的引用,不能给 exports 赋值,否则会切断引用关系。
2.3 运行机制
require 是同步操作。模块首次加载后会被缓存,后续 require 直接返回缓存结果。模块导出的是值的拷贝,外部修改不会影响模块内部状态。
// counter.js
let count = 0;
function increment() {
count++;
}
module.exports = {
count,
increment
};
// main.js
const counter = require('./counter');
console.log(counter.count); // 0
counter.increment();
console.log(counter.count); // 仍然是 0,因为拿到的是拷贝
2.4 适用场景
CommonJS 天生为服务端设计,同步加载机制适合 Node.js 的文件系统操作。但在浏览器端存在兼容问题,需要借助打包工具(如 Webpack、Rollup)将 CommonJS 模块转换为浏览器可识别的格式。
3. AMD 规范
3.1 诞生背景
Asynchronous Module Definition(异步模块定义)由 CommonJS 社区的草案演化而来,主要解决浏览器端的异步模块加载问题。RequireJS 是 AMD 规范的典型实现。
3.2 核心语法
AMD 使用 define 定义模块,使用 require 异步加载模块。
// math.js
define(function() {
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
return {
add,
multiply
};
});
模块也可以声明依赖:
// main.js
require(['./math'], function(math) {
console.log(math.add(2, 3)); // 输出: 5
console.log(math.multiply(4, 5)); // 输出: 20
});
3.3 运行机制
AMD 的 require 和 define 都是异步操作。浏览器加载模块时不会阻塞页面渲染,用户体验更佳。模块加载完成后执行回调函数,确保依赖被正确初始化。
// 带有依赖的模块定义
define(['./dependency'], function(dep) {
// 这里的代码在 dependency 加载完成后才执行
return function() {
dep.doSomething();
};
});
3.4 优缺点分析
AMD 的优势在于浏览器原生支持异步加载,无需打包工具即可在浏览器运行。但语法相对繁琐,require 和 define 的嵌套使用增加了代码复杂度。随着 ES6 模块的普及和打包工具的成熟,AMD 逐渐退出主流舞台。
4. ES6 模块规范
4.1 标准化历程
ES6(ECMAScript 2015)正式引入了模块系统,这是 JavaScript 语言层面的原生模块规范。现代浏览器和 Node.js(14+ 版本)均已原生支持 ES6 模块。
4.2 核心语法
ES6 模块使用 export 导出,使用 import 导入。
命名导出:
// math.js
export function add(a, b) {
return a + b;
}
export const PI = 3.14159;
默认导出:
// config.js
export default {
apiUrl: 'https://example.com',
timeout: 5000
};
导入方式:
// 导入命名导出
import { add, PI } from './math.js';
console.log(add(2, 3)); // 输出: 5
console.log(PI); // 输出: 3.14159
// 导入默认导出
import config from './config.js';
console.log(config.apiUrl); // 输出: https://example.com
// 整体导入
import * as math from './math.js';
console.log(math.add(4, 5)); // 输出: 9
4.3 静态分析特性
ES6 模块的导入导出发生在编译阶段,而非运行时。这一特性带来了重要优势:
Tree Shaking:打包工具可以在构建时识别未使用的导出并将其剔除,减小最终包体积。
// util.js
export function used() {
console.log('I am used');
}
export function unused() {
console.log('I am never used');
}
// main.js
import { used } from './util.js';
used(); // 只有 used 会被打包,unused 会被剔除
静态语法检查:import 和 export 语句必须在模块顶层,不能在条件语句或函数内部使用。这一限制使得工具能够更方便地进行静态分析和优化。
4.4 与 CommonJS 的关键区别
// CommonJS
const moment = require('moment');
module.exports = { count: 1 };
// ES6 模块
import moment from 'moment';
export const count = 1;
// 导入时不能重命名
const m = require('moment'); // 允许
import m from 'moment'; // 允许(默认导入)
// 但不能用变量动态导入
const moduleName = './math';
const math = require(moduleName); // CommonJS 允许
import(math) // 静态 import 需要 import() 语法
| 特性 | CommonJS | ES6 模块 |
|---|---|---|
| 运行环境 | Node.js(服务端) | 浏览器 + Node.js |
| 加载方式 | 同步 | 异步 |
| 导出绑定 | 值拷贝 | 实时绑定 |
| 静态分析 | 不支持 | 支持 |
| 条件加载 | 允许 | 需要 import() |
4.5 ES6 模块的实时绑定
ES6 模块导出的是值的引用,而非拷贝。这意味着外部对导入值的修改会影响模块内部状态。
// counter.js
export let count = 0;
export function increment() {
count++;
}
// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1,值发生了改变
5. 规范对比与选型建议
5.1 三种规范横向对比
| 特性 | CommonJS | AMD | ES6 模块 |
|---|---|---|---|
| 诞生时间 | 2009 | 2011 | 2015 |
| 加载方式 | 同步 | 异步 | 异步 |
| 浏览器支持 | 需转译 | 原生支持 | 现代浏览器原生支持 |
| Node.js 支持 | 原生 | 需工具 | 原生 |
| 静态分析 | 弱 | 弱 | 强 |
| 当前状态 | 广泛使用 | 逐渐淘汰 | 官方推荐 |
5.2 选型指南
新项目:直接使用 ES6 模块。它是 JavaScript 的官方标准,拥有最好的生态支持和工具链优化。
现有 Node.js 项目:可以逐步迁移到 ES6 模块。在 package.json 中设置 "type": "module" 即可启用原生模块支持。
浏览器直接加载:如果不想使用打包工具,ES6 模块是最佳选择,只需在 <script> 标签中添加 type="module":
<script type="module">
import { add } from './math.js';
console.log(add(2, 3));
</script>
遗留系统维护:如果项目依赖 RequireJS 或大量 CommonJS 代码,不必急于迁移。现有的打包工具可以很好地处理这些规范,保持系统稳定运行更重要。
6. 实践建议
6.1 统一模块风格
在同一个项目中混合使用多种模块规范会增加维护成本。建议统一使用 ES6 模块,并在需要时通过打包工具处理兼容性问题。
6.2 合理拆分模块
模块粒度不宜过细或过粗。一个模块应该包含一组相关功能,对外暴露清晰的接口。例如,数据获取逻辑可以封装为一个模块,UI 组件逻辑封装为另一个模块。
6.3 使用路径别名
借助打包工具的路径别名功能,简化模块导入路径:
// 替代 from '../../../../utils'
import { formatDate } from '@/utils';
6.4 注意循环依赖
三种规范都存在循环依赖问题。ES6 模块通过实时绑定在一定程度上缓解了这个问题,但应尽量避免模块间的循环引用,保持依赖图的有向无环性。

暂无评论,快来抢沙发吧!