文章目录

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

发布于 2026-04-06 03:45:34 · 浏览 9 次 · 评论 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;
};

注意exportsmodule.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 的 requiredefine 都是异步操作。浏览器加载模块时不会阻塞页面渲染,用户体验更佳。模块加载完成后执行回调函数,确保依赖被正确初始化。

// 带有依赖的模块定义
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 会被剔除

静态语法检查importexport 语句必须在模块顶层,不能在条件语句或函数内部使用。这一限制使得工具能够更方便地进行静态分析和优化。

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)

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

扫一扫,手机查看

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