怎么构建模块化JavaScript代码:CommonJS与ES Module对比

作者:接口漫谈

时间:2026-10-30

浏览:194

深入对比CommonJS与ES Module的核心差异,解析加载机制、导出引用原理、工程配置转换及现代JavaScript项目模块化架构最佳实践。

文章目录
构建模块化 JavaScript 代码的核心在于根据目标运行环境与工程化诉求,合理选择并规范使用 CommonJS 或 ES Module;通过理清两者的加载机制、导出引用原理以及运行环境配置,开发者可以设计出高内聚、易维护且无依赖冲突的现代化代码架构。

一、模块化演进与两大规范概览

早期的 JavaScript 缺乏语言层面的模块管理机制,依赖全局变量与脚本顺序加载,极易产生命名空间污染和难以追踪的依赖链问题。随着服务端的 Node.js 兴起与前端工程规模膨胀,两大主流模块规范成为了 JavaScript 生态的基石:

  • CommonJS (CJS):Node.js 早期默认采用的规范,通过 require() 同步加载模块,并通过 module.exports 或 exports 导出对象。专为服务端本地文件系统设计,执行即加载。
  • ES Module (ESM):ECMAScript 2015 (ES6) 正式确立的原生官方规范,使用 import 与 export 关键字,支持静态编译分析与异步网络获取,是现代浏览器与现代 Node.js 共同推荐的标准。
怎么构建模块化JavaScript代码:CommonJS与ES Module对比
图1:CommonJS 与 ES Module 架构属性与机制核心对比

二、核心机制对比:值拷贝与实时只读引用

两者的根本区别不仅在于语法形式,更在于模块导出值的底层引用模型:

  • CommonJS 输出的是值的浅拷贝:当模块完成加载后,被导入变量只是原有值的一个快照。即使导出模块内部后续改变了该变量的值,导入方接收到的变量副本也不会随之更新,除非模块重新导出一个函数去动态获取。
  • ES Module 输出的是值的实时绑定 (Live Binding):导入的变量本质上是指向导出模块内存地址的只读指针。当原始模块内部修改该变量时,导入方访问到的数值会实时同步更新;同时,导入方不能直接给该导入变量重新赋值。
怎么构建模块化JavaScript代码:CommonJS与ES Module对比
图2:CommonJS 值拷贝与 ES Module 实时引用(Live Binding)机制差异示意

代码实例:验证值传递特征

以下两组代码分别展示了在 CJS 与 ESM 下修改导出变量时的实际运行效果:

// ==================== CommonJS 示例 ====================
// counter.js
let count = 1;
function increment() { count++; }
module.exports = { count, increment };

// main.js
const { count, increment } = require('./counter');
console.log(count); // 输出示例: 1
increment();
console.log(count); // 输出示例: 1 (未改变,因为导入的是浅拷贝快照)

// ==================== ES Module 示例 ====================
// counter.mjs
export let count = 1;
export function increment() { count++; }

// main.mjs
import { count, increment } from './counter.mjs';
console.log(count); // 输出示例: 1
increment();
console.log(count); // 输出示例: 2 (实时同步变化)
// count = 10;      // 报错: TypeError: Assignment to constant variable.

三、实操步骤:构建现代模块化 JavaScript 工程

为了构建清晰可控的模块化架构,开发者通常需要经历从目录规划、语法选型、配置定义到打包验证的完整流程。以下为建立现代 ESM 项目并保持向下兼容的标准步骤:

怎么构建模块化JavaScript代码:CommonJS与ES Module对比
图3:模块化 JavaScript 项目构建全流程

步骤 1:初始化与模块格式声明

在 Node.js 环境中,文件默认会被当作 CommonJS 处理。若要直接以原生 ES Module 方式运行,需在项目根目录的 package.json 中显式声明:

{
  "name": "js-module-demo",
  "version": "1.0.0",
  "type": "module"
}

注意:当设置了 "type": "module" 后,项目内所有的 .js 文件均会被当作 ESM 解析;若个别文件需保持 CommonJS 运行,需将后缀显式命名为 .cjs。反之,未设置时若要写 ESM,需将文件命名为 .mjs。

步骤 2:设计高内聚的子模块与具名导出

编写功能模块时,推荐使用具名导出(Named Export)而非大单体 export default。具名导出具有确切的符号标识,不仅能避免导入时随意命名导致的混乱,还能让打包工具(如 Rollup、Webpack、Vite)在静态分析阶段精准剔除未使用的导出。

// src/math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

步骤 3:使用聚合入口提供干净接口

在模块目录下建立 index.js,统一汇聚对外开放的功能,防止调用方深入深层相对路径去引用内部文件:

// src/index.js
export { add, subtract } from './math.js';
// 保持完整相对路径与扩展名 .js,在原生 ESM 环境中不可省略文件后缀

步骤 4:配置双格式导出支持(Dual Package Hazard 预防)

如果您正在开发一个 npm 库并希望同时支持 require() 和 import 用户,可以在 package.json 中使用现代条件导出字段 exports:

{
  "name": "my-library",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "exports": {
    "import": "./dist/index.mjs",
    "require": "./dist/index.cjs"
  }
}

四、选型对比与综合总结

在面对不同的实际业务场景时,应该如何在这两者之间做出抉择?下表总结了关键维度的决策基准:

对比维度 CommonJS (CJS) ES Module (ESM)
标准来源 社区标准(Node.js 早期默认) ECMAScript 官方规范(全平台通用)
加载方式 运行时同步加载(I/O 阻塞) 静态编译解析 + 支持动态 import() 异步加载
Tree-Shaking 支持 困难(动态运行时特性无法提前剪枝) 原生强支持(静态 AST 分析)
顶级 Await 不支持 原生支持(Top-level await)
循环引用处理 返回已执行部分的副本快照 返回未初始化引用的死区(Temporal Dead Zone)
推荐场景 旧版 Node 脚本、传统构建插件 新项目开发、前端现代应用、通用跨端类库
怎么构建模块化JavaScript代码:CommonJS与ES Module对比
图4:模块化技术选型与工程演进决策链路

五、结语

JavaScript 模块化已经全面步入以 ES Module 为核心标准的时代。尽管由于历史庞大的生态资产,CommonJS 仍广泛存在于服务端脚本及传统工具链中,但在新项目设计中,以 ESM 规范为基准进行模块解耦与接口抽象,并通过构建工具提供向下兼容能力,是保证代码高质量、低耦合以及长久可维护性的最佳技术路线。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章