文章目录

JavaScript 模块化:CommonJS、AMD、ES6 模块

发布于 2026-04-06 03:45:34 · 浏览 165 次 · 评论 0 条

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 模块通过实时绑定在一定程度上缓解了这个问题,但应尽量避免模块间的循环引用,保持依赖图的有向无环性。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文