JavaScript模块系统详解:CommonJS与ES Module的区别

作者:终端旅客

时间:2026-10-30

浏览:258

深入解析JavaScript两大主流模块规范CommonJS与ES Module的核心差异,涵盖语法规范、加载时机、导出机制、循环引用处理及现代工程选择策略。

文章目录
在 JavaScript 生态中,CommonJS 与 ES Module 是最为核心的两种模块化规范。CommonJS 诞生于服务端 Node.js 环境,采用同步运行时加载与值拷贝机制;而 ES Module 是 ECMAScript 官方标准,基于静态编译期分析实现模块图解析与动态只读绑定。本文将从加载时机、值传递方式、循环引用处理及实际工程决策四个维度,全面拆解两者的本质差异。

一、为什么 JavaScript 会存在两套主流模块标准

早期的 JavaScript 并没有语言内置的模块系统,代码往往通过全局作用域或立即执行函数(IIFE)组织,极易引发命名冲突与依赖维护灾难。随着 Node.js 在 2009 年诞生,社区制定了面向服务端的 CommonJS(简称 CJS)规范,允许通过 require() 和 module.exports 同步加载本地文件模块。

随后,ECMAScript 在 2015 年(ES6)推出了官方模块标准 ECMAScript Modules(简称 ESM),使用 import 与 export 关键字,确立了统一浏览器与服务端模块体系的目标。现代 JavaScript 生态正逐步从 CommonJS 向 ES Module 迁移,但两者在设计目标与执行模型上有着根本性的分歧。

JavaScript模块系统详解:CommonJS与ES Module的区别
图 1:CommonJS 与 ES Module 从文件解析到执行的步骤流转差异

二、核心差异一:语法形态与加载时机

CommonJS 模块加载是动态且同步的。require() 是一个内置函数,可以在代码的任意位置调用,例如放置在 if 分支内部或函数执行体中。这也意味着其导入路径可以动态拼接:

// CommonJS 动态加载示例 (运行时解析)
const configName = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';
const config = require(`./config.${configName}.js`);

与此相对,ES Module 的导入导出是静态声明式的。标准 import 声明必须置于模块的顶层作用域,导入路径必须是固定的字符串字面量,不能包含变量。JavaScript 引擎在解析代码阶段(即执行代码之前)就会扫描所有的 import 和 export 语句,提前构建模块依赖拓扑图。

// ES Module 静态导入示例 (语法分析阶段确定)
import { queryUser, updateUser } from './userService.js';
export const API_VERSION = 'v1';

静态导入的特性赋予了现代打包工具(如 Webpack、Rollup、Vite)进行 Tree-shaking(死代码消除) 的能力。因为模块在未执行前就已经确定了导出与导入的引用关系,打包器可以安全地剔除未被使用的导出函数;而 CommonJS 导出的对象具有动态修改的可能,无法可靠进行静态剔除。

注意事项:尽管 ES Module 主打静态解析,但它也提供了 import() 动态函数。import() 返回一个 Promise 对象,用于需要懒加载或条件加载的场景,与静态 import ... from ... 语法并行不悖。

三、核心差异二:导出机制与内存值传递

很多开发者在日常使用中最容易踩坑的地方,在于两者导出数据时对变量内存的处理方式。

  • CommonJS 导出的是“值的浅拷贝”:当一个模块执行 module.exports = { count } 时,输出的是该变量在当时时间点的浅拷贝副本。即使源模块内部后续修改了 count,导入方获取到的依然是最初拷贝的值。
  • ES Module 导出的是“只读的动态引用(Live Binding)”:export 并不会复制数据,而是在内存中建立一个指针绑定。当源模块修改其内部变量时,外部所有引用该模块的地方都能即时读取到最新的值;但导入方不能对该绑定进行直接赋值修改。
JavaScript模块系统详解:CommonJS与ES Module的区别
图 2:CommonJS 的浅拷贝传值与 ES Module 实时只读指针绑定的内存示意

下面通过具体代码进行验证:

CommonJS 值拷贝验证代码

// counter-cjs.js
let count = 1;
function increment() {
  count++;
}
module.exports = { count, increment };

// main-cjs.js
const { count, increment } = require('./counter-cjs.js');
console.log(count); // 输出: 1
increment();
console.log(count); // 依然输出: 1 (外部只保留了最初解构出来的基础类型拷贝)

ES Module 实时绑定验证代码

// counter-esm.js
export let count = 1;
export function increment() {
  count++;
}

// main-esm.js
import { count, increment } from './counter-esm.js';
console.log(count); // 输出: 1
increment();
console.log(count); // 输出: 2 (实时读取到模块内部的最新状态)
// count = 10;      // 报错: TypeError: Assignment to constant variable.

四、核心差异三:循环引用(Circular Dependency)的处理

当模块 A 引用模块 B,同时模块 B 又引用模块 A 时,两种规范对这种循环依赖采取了截然不同的解决方案。

CommonJS 基于已执行部分缓存导出:当遇到循环 require() 时,不会产生死循环,而是立即返回目标模块当前已经执行完毕的 exports 属性。如果某些属性在该时间点尚未执行到,则会得到 undefined。

ES Module 基于引用指针延迟取值:由于 ESM 在静态编译阶段就已经完成了模块连接(Linking),符号插槽已在内存中创建。代码在求值阶段自下而上执行时,只要在具体访问该变量的时刻对应代码已经执行完毕,就可以安全获取到值。

JavaScript模块系统详解:CommonJS与ES Module的区别
图 3:循环依赖时 CommonJS 中断导出与 ES Module 符号提升引用的拓扑对比

五、其他关键差异维度

除了加载方式和数据流向,在 Node.js 运行时中混用这两种规范时,还会遇到以下具体差异:

对比维度 CommonJS (CJS) ES Module (ESM)
官方规范制定方 Node.js 社区 / CommonJS 规范组织 TC39(ECMAScript 国际语言标准)
顶层 this 指向 指向当前模块的 exports 对象 在顶层严格指向 undefined
环境预置变量 自带 __filename, __dirname 无预置变量,需用 import.meta.url 转换
顶层 Await 支持 不支持(必须包裹在 async 函数内) 原生支持 Top-level Await
跨模块混用 无法使用 require() 同步加载纯 ESM 模块 可直接使用 import 或 createRequire 加载 CJS

六、现代工程选型与迁移建议

随着 Node.js 12+ 对 ES Module 的原生支持与工具链的完善,在实际工程中选择模块系统时可以遵循以下实践准则:

  1. 前端工程与现代应用开发:全面优先采用 ES Module。现代浏览器全部原生支持 ESM,且主流打包构建工具(Vite、Webpack 5)依赖 ESM 静态结构实现急速热更新(HMR)与 Tree-shaking 优化。
  2. Node.js 新建后端服务:建议在 package.json 中配置 "type": "module",直接以 ESM 模式编写服务端逻辑,享受标准语法与顶层 await 的便利。
  3. 公共 NPM 基础库开发:推荐采用双模式导出(Dual Package)。使用现代打包工具输出 ESM 与 CJS 两套构建产物,并在 package.json 的 exports 字段中进行精确条件分发,确保老旧 CJS 项目与现代 ESM 项目均可无缝引入。
JavaScript模块系统详解:CommonJS与ES Module的区别
图 4:CommonJS 与 ES Module 在现代工程实践中的应用选型决策模型

七、总结

回顾两者,CommonJS 以简单直观的运行时同步加载解决了服务端初期模块化的问题,但其动态特性限制了现代前端构建与死代码剔除;ES Module 则凭借其严谨的静态结构、实时符号绑定以及对浏览器运行时的原生适配,成为了现代 JavaScript 生态的基石。

理解两者的运行生命周期、值引用模型及循环依赖机制,能够帮助开发者在配置工程工具、排查生产环境怪异导入 Bug 以及设计通用 SDK 时更加得心应手。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章