谈谈 Node.js 中的模块系统,CommonJS 和 ES Modules 的区别是什么?

devtools/2025/2/28 6:28:00/

Node.js 模块系统:CommonJS 和 ES Modules 核心差异与实战指南

一、模块系统基础概念

**CommonJS (CJS)**​ 是 Node.js 传统模块系统,采用同步加载方式,典型特征:

// 导出
module.exports = { name: 'cjs' };  // 或 exports.name = 'cjs'// 导入
const moduleA = require('./moduleA');  // 动态语法

**ES Modules (ESM)**​ 是 ECMAScript 标准模块系统,采用异步加载,典型特征:

// 导出
export const name = 'esm';  // 命名导出
export default { version: 1 };  // 默认导出// 导入
import moduleB, { name } from './moduleB.mjs';  // 静态语法
二、7 个关键差异点(附代码验证)
1. 语法与加载机制
  • CJS 动态加载:允许条件语句中 require
if (Math.random() > 0.5) {require('./moduleA');  // 运行时决定加载
}
  • ESM 静态分析:import 必须顶层声明
// 报错:import 必须位于模块顶部
if (condition) { import './moduleB.mjs' } 
2. 模块作用域差异
  • CJS 非严格模式:变量可隐式创建全局变量
// module-cjs.js
undeclaredVar = 100;  // 不报错,污染全局
  • ESM 严格模式:禁止隐式全局变量
// module-esm.mjs
undeclaredVar = 100; // 报错:未定义变量
3. 循环引用处理
  • CJS 动态引用:可能拿到未初始化的模块
// a.js
exports.loaded = false;
const b = require('./b');
console.log('在a中,b.loaded =', b.loaded);  // true
exports.loaded = true;// b.js
exports.loaded = false;
const a = require('./a');
console.log('在b中,a.loaded =', a.loaded);  // false
exports.loaded = true;// 执行 node a.js → 输出顺序:
// 在b中,a.loaded = false
// 在a中,b.loaded = true
  • ESM 静态绑定:引用指向最新值(类似指针)
// a.mjs
import { loaded } from './b.mjs';
export let loaded = false;
console.log('在a中,b.loaded =', loaded);  // true
loaded = true;// b.mjs
import { loaded } from './a.mjs';
export let loaded = false;
console.log('在b中,a.loaded =', loaded);  // false
loaded = true;// 执行 node a.mjs → 报错(循环引用需特殊处理)
4. 顶层 this 指向
  • CJS 的 this​ 指向 module.exports 对象
console.log(this === module.exports);  // true
  • ESM 的 this​ 为 undefined(严格模式)
console.log(this);  // undefined
5. 文件扩展名与配置
  • CJS​ 默认识别 .js 和 .cjs 文件
  • ESM​ 需要以下条件之一:
    • 文件后缀为 .mjs
    • 最近的 package.json 中设置 "type": "module"
// package.json
{"type": "module"  // 项目内 .js 文件默认视为 ESM
}
6. 引用类型差异
  • CJS 导出值拷贝:基本类型值复制,对象类型浅拷贝
// cjs-module.js
let count = 1;
setTimeout(() => { count = 2 }, 100);
module.exports = { count };// main.js
const { count } = require('./cjs-module');
console.log(count);  // 1
setTimeout(() => console.log(count), 200);  // 仍为1
  • ESM 动态绑定:始终获取最新值
// esm-module.mjs
export let count = 1;
setTimeout(() => { count = 2 }, 100);// main.mjs
import { count } from './esm-module.mjs';
console.log(count);  // 1
setTimeout(() => console.log(count), 200);  // 变为2
7. 动态导入能力
  • CJS​ 原生不支持动态导入,但可通过 require 实现
  • ESM​ 支持 import() 动态导入(返回 Promise)
// 动态加载 ESM 模块
const module = await import('./module.mjs');// 动态加载 CJS 模块(在 ESM 中)
import cjsModule from './cjs-module.cjs';  // 需完整后缀

三、日常开发建议
1. 新项目技术选型
  • 优先使用 ESM:符合语言标准,支持 Tree Shaking
// package.json
{"type": "module","scripts": {"start": "node --experimental-vm-modules src/index.mjs"}
}
2. 旧项目迁移策略
  • 渐进式迁移
    • 将单个文件后缀改为 .mjs 或设置 "type": "module"
    • 使用 import/export 语法逐步替换
// 混合使用示例(在 ESM 中引入 CJS)
import cjsModule from './legacy-module.cjs';  // 注意后缀
3. 模块兼容性处理
  • 双格式发布库:通过 package.json 指定双入口
{"exports": {"import": "./esm-module.mjs","require": "./cjs-module.cjs"}
}
4. 避免踩坑指南
  • 禁用默认互操作:CJS 默认导出需特别注意
// ESM 导入 CJS 模块
import cjsModule from './cjs-module.cjs';  // module.exports 整体作为默认导出
  • 循环引用处理:ESM 中建议使用函数封装初始化逻辑
// a.mjs
import { initB } from './b.mjs';
export let valueA = '未初始化';export function initA() {valueA = '初始化A';initB();
}// b.mjs
import { initA } from './a.mjs';
export let valueB = '未初始化';export function initB() {valueB = '初始化B';initA();  // 安全调用
}

四、注意事项
  1. 全局变量替换
    ESM 中无法直接使用 __dirname,需改用:

    import { fileURLToPath } from 'url';
    const __dirname = path.dirname(fileURLToPath(import.meta.url));
  2. 文件扩展名强制要求
    在 ESM 中引入文件时必须写完整扩展名:

    import './utils.js';  // 必须写 .js
  3. 默认导出差异
    CJS 的 module.exports 对应 ESM 的默认导出:

    // CJS 模块
    module.exports = { a: 1 };// ESM 导入方式
    import cjsModule from './cjs-module.cjs';  // { a: 1 }
  4. 性能优化
    ESM 的静态分析特性使打包工具(如 Rollup)能实现更高效的 Tree Shaking。


五、总结

理解两种模块系统的核心差异,能帮助开发者根据场景合理选择:

  • CJS​ 适合传统 Node.js 项目、需要动态加载的场景
  • ESM​ 适合现代浏览器兼容项目、需要静态分析的构建优化

在混合项目中,通过文件扩展名和 package.json 配置明确模块类型,避免隐式错误。对于长期维护的项目,逐步向 ESM 迁移是更符合技术趋势的选择。


http://www.ppmy.cn/devtools/163282.html

相关文章

【Uniapp-Vue3】开发userStore用户所需的相关操作

在项目根路径下创建的stores文件夹中创建user.js文件 并将以下内容复制到user.js中 import {ref} from "vue" import { defineStore } from pinia; const uniIdCo uniCloud.importObject("uni-id-co") const db uniCloud.database(); const usersTable…

数据库MySQL

【解决问题】mysql提示不是内部或外部命令,也不是可运行的程序 一般这种问题是因为没有在系统变量里面添加MySQL的可执行路径 以下是添加可执行路径的方法: 第一步:winR输入services.msc 然后找到MySQL,右击属性并复制MySQL的可执…

【蓝桥杯集训·每日一题2025】 AcWing 5438. 密接牛追踪2 python

5438. 密接牛追踪2 Week 2 2月26日 题目描述 农夫约翰有 N N N 头奶牛排成一排,从左到右依次编号为 1 ∼ N 1 \sim N 1∼N。 不幸的是,有一种传染病正在蔓延。 最开始时,只有一部分奶牛受到感染。 每经过一个晚上,受感染的牛…

Visual Studio更新说明(关注:.NET+AI生产力)

Ver V0.0:Visual Studio 2022 v17.12更新:.NET9AI生产力 AI插件推荐 (1)腾讯云AI代码手(内含了DeepSeek-R1),目前免费,但收费我也可能会买。 AI插件!推荐 (1)百度的…

销售易NeoCRM与八骏科技CRM:全方位深度对比

在当今竞争激烈的CRM市场中,销售易NeoCRM和八骏科技CRM作为国内知名的CRM解决方案,各自拥有独特的优势和特点。本文将从功能、用户体验、价格、市场评价以及适用场景等方面对这两款CRM系统进行对比总结和盘点。 一、功能对比 销售易NeoCRM:…

嵌入式硬件篇---常用的汇编语言指令

文章目录 前言汇编语言简介1. 数据传送指令MOVPUSHPOPXCHG 2. 算术运算指令ADDSUBMULDIVINCDEC 3. 逻辑运算指令ANDORXORNOTSHL/SHR 4. 控制转移指令JMPCALLRETJE/JZJNE/JNZJG/JNLEJL/JNGE 5. 比较与测试指令CMPTEST 6. 标志寄存器操作指令STCCLCSTDCLD 7. 字符串操作指令MOVSL…

康威生命游戏

通过二维卷积快速计算每个细胞的Moore型邻居存活数(8邻域) import numpy as np import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation from scipy.signal import convolve2d import time import logginglogging.basicConfi…

Spring 核心技术解析【纯干货版】- XIV:Spring 消息模块 Spring-Jms 模块精讲

在现代分布式系统中,消息队列(Message Queue,MQ)扮演着至关重要的角色,它不仅能够解耦系统各个模块,还能提升系统的可扩展性和可靠性。JMS(Java Message Service)作为 Java EE 规范中…