Node.js模块系统:CJS与ESM差异解析(本文章由ai生成)
·
Node.js 支持两种主要的模块系统:CommonJS(CJS)和 ES6 模块(ESM)。理解它们的设计理念、语法差异以及正确的使用场景,对于构建现代、高效的 Node.js 应用至关重要。
一、核心差异对比
首先,我们可以通过一个表格来清晰地对比两者的主要区别:
| 特性 | CommonJS (CJS) | ES6 模块 (ESM) | 说明与影响 |
|---|---|---|---|
| 设计初衷 | 为服务器端(Node.js)设计 | 为浏览器和服务器端统一设计 | ESM 旨在成为 JavaScript 的官方模块标准 。 |
| 加载时机 | 运行时加载(动态) | 编译时加载(静态) | CJS 可以条件性导入,ESM 的 import 必须在顶层,利于优化(如 Tree Shaking)。 |
| 加载行为 | 同步加载 | 异步加载 | CJS 在 Node.js 中同步读取模块文件,ESM 支持异步,更适合浏览器环境 。 |
| 导出方式 | module.exports 或 exports 对象 |
export 或 export default 语句 |
语法本质不同,不可混用 。 |
| 导入方式 | require() 函数 |
import 语句 |
require 是函数调用,import 是声明语句 。 |
| 输出本质 | 值的拷贝(对基本类型) | 值的引用(只读) | CJS 输出的是缓存副本,ESM 输出的是动态绑定的引用 。 |
顶层 this |
指向当前模块 | 指向 undefined |
安全性差异。 |
| 文件扩展名 | .js 或 .cjs |
.mjs 或在 package.json 中设置 “type”: “module” |
Node.js 通过扩展名或配置识别模块类型 。 |
| 循环依赖处理 | 支持,但可能得到不完整的模块 | 支持,引用是动态绑定的 | ESM 处理循环依赖更符合预期 。 |
二、语法与正确使用方式
1. CommonJS 模块系统
CommonJS 是 Node.js 长期以来的默认模块系统,其核心是 module.exports 和 require()。
导出模块:
// math-cjs.js
// 方式1:导出一个对象
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
module.exports = { add, subtract };
// 方式2:逐个添加属性到 exports 对象
exports.add = add;
exports.subtract = subtract;
// 方式3:导出一个类
class Calculator {
multiply(a, b) { return a * b; }
}
module.exports = Calculator;
导入模块:
// app-cjs.js
// 方式1:导入整个模块对象
const math = require('./math-cjs');
console.log(math.add(5, 3)); // 输出: 8
// 方式2:解构导入(推荐,清晰)
const { add, subtract } = require('./math-cjs');
console.log(subtract(5, 3)); // 输出: 2
// 方式3:导入类
const Calc = require('./math-cjs'); // 假设导出的是Calculator类
const calc = new Calc();
console.log(calc.multiply(5, 3)); // 输出: 15
关键点:
require()可以出现在代码的任何位置(包括条件判断中),因为是运行时同步执行 。- 模块第一次被
require后会被缓存,后续调用require会直接返回缓存结果 。
2. ES6 模块系统
ES6 模块是 ECMAScript 标准,需要通过特定方式在 Node.js 中启用。
启用 ESM:
有两种方式:
- 文件扩展名法:将模块文件后缀命名为
.mjs。 - 项目配置法:在项目的
package.json文件中添加"type": "module"字段,则所有.js文件都将被视为 ESM 。
导出模块:
// math-esm.mjs (或 .js 在 type: module 项目中)
// 命名导出 (Named Export)
export const PI = 3.14159;
export function multiply(a, b) { return a * b; }
// 默认导出 (Default Export)
class Geometry {
circleArea(r) { return PI * r * r; }
}
export default Geometry;
// 也可以先定义,后统一导出
const secret = 42;
function divide(a, b) { return a / b; }
export { secret, divide };
导入模块:
// app-esm.mjs (或 .js 在 type: module 项目中)
// 导入命名导出
import { multiply, PI } from './math-esm.mjs';
console.log(multiply(PI, 2)); // 输出: 6.28318
// 导入默认导出
import Geometry from './math-esm.mjs';
const geo = new Geometry();
console.log(geo.circleArea(2)); // 输出: 12.56636
// 导入所有命名导出到一个命名空间对象
import * as mathUtil from './math-esm.mjs';
console.log(mathUtil.secret); // 输出: 42
// 动态导入 (返回一个Promise)
async function loadModule() {
const module = await import('./math-esm.mjs');
console.log(module.divide(10, 2)); // 输出: 5
}
loadModule();
关键点:
import语句必须出现在模块的顶层作用域,不能嵌套在条件语句中(动态import()函数除外),这是静态分析的基础 。- ESM 模块中不能使用
require,exports,module.exports,__filename,__dirname等 CJS 全局变量。如需__dirname,可用import.meta.url构造 。
三、互操作性(混合使用)
在 Node.js 项目中,两种模块可能共存。Node.js 提供了有限的互操作支持,但规则需要严格遵守。
-
在 ESM 中导入 CJS 模块:
- 大多数情况下,ESM 可以
importCJS 模块。 - CJS 模块的
module.exports会被当作 ESM 的默认导出(default export)。
// esm-app.mjs import cjsModule from './cjs-module.js'; // 正确,导入的是整个module.exports // import { named } from './cjs-module.js'; // 错误!除非CJS模块使用了特殊编译工具 console.log(cjsModule); - 大多数情况下,ESM 可以
-
在 CJS 中导入 ESM 模块(限制):
- CommonJS 模块不能使用
require()来加载 ESM 模块。 - 唯一的方式是使用 动态
import(),它会返回一个 Promise 。
// cjs-app.js async function main() { // 使用动态 import() 导入 ESM 模块 const esmModule = await import('./esm-module.mjs'); console.log(esmModule.default); // 访问默认导出 console.log(esmModule.namedExport); // 访问命名导出 } main(); - CommonJS 模块不能使用
四、应用场景与最佳实践建议
| 场景 | 推荐模块系统 | 理由 |
|---|---|---|
| 新的 Node.js 项目 | ES6 模块 (ESM) | 这是 JavaScript 的语言标准,代表未来方向,支持静态分析和 Tree Shaking,有助于构建更优的应用 。 |
| 维护遗留 Node.js 项目 | CommonJS (CJS) | 保持一致性,避免不必要的重构风险和兼容性问题。 |
| 开发供他人使用的库 | 提供双模式 | 在 package.json 中使用 “exports” 字段分别指定 . (import) 和 . (require) 的入口点,以同时支持两种用户。 |
| 需要条件加载或插件化 | CommonJS (CJS) | 其动态 require() 特性在此类场景下更灵活 。 |
| 前端构建工具链项目 | ES6 模块 (ESM) | 与 Vite、Webpack、Rollup 等现代构建工具生态结合更紧密,利于 Tree Shaking 优化 。 |
最佳实践总结:
- 明确声明:在项目根目录的
package.json中明确设置“type”: “module”或“type”: “commonjs”,或统一使用.mjs/.cjs扩展名来避免歧义 。 - 避免混用:在同一个项目中,尽量统一使用一种模块系统,以降低复杂度和维护成本。
- 理解差异:深刻理解“动态加载 vs 静态加载”、“值拷贝 vs 值引用”等核心差异,这会影响模块间的数据交互和状态管理 。
- 工具辅助:使用 ESLint、TypeScript 等工具可以帮助检测和避免模块混用导致的错误。
选择哪种模块系统取决于项目类型、团队约定和长期维护计划。对于全新的项目,拥抱 ES6 模块是更面向未来的选择 。
参考来源
- Node.js完全指南:从入门到精通
- Node JS 模块:Node.js 需求与导入
- 前端Javascript模块化 CommonJS与ES Module区别
- 前端面试——CommonJS模块和ES6模块的区别?
- 【JavaScript】JavaScript模块化开发:ES6模块与CommonJs的对比与应用
- commonJS和ES6模块化的区别
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)