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

server/2025/3/4 3:23:14/

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/server/172237.html

相关文章

内网渗透测试-Vulnerable Docker靶场

靶场来源: Vulnerable Docker: 1 ~ VulnHub 描述:Down By The Docker 有没有想过在容器中玩 docker 错误配置、权限提升等? 下载此 VM,拿出您的渗透测试帽并开始使用 我们有 2 种模式: - HARD:这需要您将 d…

SoapUI 结合 Postman 测试 WebService 协议

SoapUI 结合 Postman 测试 WebService 协议 一、WebService 协议概述 WebService 是一种基于标准的 Web 应用程序接口,允许不同系统之间通过网络进行通信和数据交换。常见的 WebService 协议有 SOAP(Simple Object Access Protocol)&#x…

C++对象特性

#构造函数 和 析构函数 构造函数:主要为对象属性赋值 语法:类名(){} 注意: 1.无返回值也无void 2.函数名称与类名相同 析构函数 语法:~类名(){} 注意: 1.无返回值也无void 2.不可以有参数&#xff0c;不可发生重载 class Person { public://构造函数Person(){cout<<&quo…

C语言入门资料分享源码+PDF速查手册

01 目标&#xff1a;掌握基础语法&#xff0c;能编写简单的程序 源码PDF获取 通过网盘分享的文件&#xff1a;C语言入门到精通.rar 链接: https://pan.baidu.com/s/1lcKj3aywRJUecLmoDeQfFg?pwdxiyx 提取码: xiyx 02 环境搭建 安装编译器&#xff08;推荐GCC/MinGW/M…

Trae智能协作AI编程工具IDE:如何在MacBook Pro下载、安装和配置使用Trae?

Trae智能协作AI编程工具IDE&#xff1a;如何在MacBook Pro下载、安装和配置使用Trae&#xff1f; 一、为什么选择Trae智能协作IDE&#xff1f; 在AI编程新时代&#xff0c;Trae通过以下突破性功能重新定义开发体验&#xff1a; 双向智能增强&#xff1a;AI不仅提供代码补全&a…

Ubuntu 下 nginx-1.24.0 源码分析 - ngx_init_cycle 函数 - 详解(6)

详解&#xff08;6&#xff09; 初始化监听套接字数组&#xff08;listening&#xff09; n old_cycle->listening.nelts ? old_cycle->listening.nelts : 10;if (ngx_array_init(&cycle->listening, pool, n, sizeof(ngx_listening_t))! NGX_OK){ngx_destroy_p…

SQL Server详细使用教程(包含启动SQL server服务、建立数据库、建表的详细操作) 非常适合初学者

SQL Server详细使用教程(包含启动SQL server服务、建立数据库、建表的详细操作) 非常适合初学者 文章目录 目录 前言 一、启动SQL server服务的三种方法 1.不启动SQL server服务的影响 2.方法一&#xff1a;利用cmd启动SQL server服务 3.方法二&#xff1a;利用SQL Serv…

基于SpringBoot的“同城宠物照看系统”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“同城宠物照看系统”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 系统总体结构图 局部E-R图 系统首页界面 系统…