模块化:ES Module 与 CommonJS 的区别

一、基础对比表

维度 CommonJS (CJS) ES Module (ESM)
环境 Node.js 默认模块系统 浏览器原生 + Node.js(.mjs 或 type:module)
语法 require() / module.exports import / export
加载时机 运行时加载 编译时加载(静态分析)
输出方式 值的拷贝 值的只读引用(live binding)
顶级 this 指向当前模块 undefined
异步加载 同步(主要) 异步(浏览器环境)
Tree Shaking 不支持 支持(静态结构)
循环依赖 能工作,但获取到的是部分值 支持,通过引用绑定

二、语法差异

js 复制代码
// ===== CommonJS =====
// 导出
module.exports = { a: 1, b: 2 };
// 或者
exports.c = 3;

// 导入
const obj = require('./module');
const { a, b } = require('./module');


// ===== ES Module =====
// 导出
export const a = 1;
export const b = 2;
export default function() { };
export { c as d };

// 导入
import obj from './module';            // 默认导入
import { a, b } from './module';      // 具名导入
import * as all from './module';       // 命名空间导入
import obj, { a } from './module';    // 混合导入

三、核心区别详解

1. 值拷贝 vs 值引用(关键)

js 复制代码
// ===== CommonJS:值拷贝 =====
// counter.js
let count = 0;
function add() { count++; }
module.exports = { count, add };

// main.js
const { count, add } = require('./counter');
console.log(count); // 0
add();
console.log(count); // 仍为 0(拷贝的是值的快照)


// ===== ES Module:实时引用 =====
// counter.mjs
export let count = 0;
export function add() { count++; }

// main.mjs
import { count, add } from './counter.mjs';
console.log(count); // 0
add();
console.log(count); // 1(实时绑定,获取最新值)

解释

  • CJS 在 require 时,将导出值拷贝一份给导入方,后续变化互不影响。
  • ESM 的 import活的只读引用,导入方始终能拿到模块内部的最新值(但不能修改)。

2. 运行时加载 vs 编译时加载

js 复制代码
// CommonJS:条件加载(运行时)
if (condition) {
  const module = require('./moduleA'); // 可以
}


// ES Module:静态加载(编译时)
import moduleA from './moduleA'; // 只能在顶层,不能放入条件/循环

// 需要条件加载时,用动态 import()
if (condition) {
  const module = await import('./moduleA.js');
}

静态结构的优势

  • 支持 Tree Shaking(打包时去除未使用代码)。
  • 支持静态分析和类型推导。
  • 浏览器可以提前解析并预加载依赖。

3. 循环依赖行为对比

js 复制代码
// ===== CommonJS 循环依赖 =====
// a.js
exports.done = false;
const b = require('./b.js');
console.log('a.js: b.done =', b.done);
exports.done = true;

// b.js
exports.done = false;
const a = require('./a.js');
console.log('b.js: a.done =', a.done);
exports.done = true;

// main.js
require('./a.js');
// 输出:
// b.js: a.done = false    (require 时 a 还没执行完,只拿到部分导出)
// a.js: b.done = true
js 复制代码
// ===== ES Module 循环依赖 =====
// a.mjs
import { bDone } from './b.mjs';
console.log('a.mjs: bDone =', bDone);
export const aDone = true;

// b.mjs
import { aDone } from './a.mjs';
console.log('b.mjs: aDone =', aDone);
export const bDone = true;

// 报错或 undefined,因为 ESM 禁止在模块解析前使用未初始化绑定
// 需要小心安排引用顺序

4. 顶层 this

js 复制代码
// CommonJS
console.log(this === module.exports); // true

// ES Module
console.log(this); // undefined

四、Node.js 中 ESM 与 CJS 互操作

CJS 加载 ESM

js 复制代码
// Node.js 中 CJS 可以通过动态 import() 加载 ESM
const esmModule = await import('./module.mjs');

ESM 加载 CJS

js 复制代码
// ESM 中可以导入 CJS 模块
import cjsModule from './module.cjs';
// 或者使用 createRequire
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const cjs = require('./module.cjs');

关键差异

  • ESM 文件中不能使用 requiremoduleexports__dirname__filename(但可通过 import.meta.url 替代)。
  • CJS 不能直接 require ESM 文件(Node.js 限制,只能通过动态 import())。

五、何时使用哪种?

场景 推荐
新项目(前端/全栈) ESM(标准化、Tree Shaking、浏览器原生支持)
NPM 库发布 同时提供 CJS + ESM 双格式
旧 Node.js 项目 保持 CJS,逐步迁移
配置/脚本 CJS 仍方便,因为可以动态 require

总结

核心记忆点:

  • CJS 输出值的拷贝 ,ESM 输出值的实时引用
  • CJS 是运行时 加载,ESM 是编译时静态分析。
  • ESM 天然支持 Tree Shaking,CJS 不行。
  • 浏览器原生支持 ESM,Node.js 两者都支持。
相关推荐
用户40950115773171 小时前
Private Forge v2.0 发布:12大前端业务场景技能系统
前端
沉默王二1 小时前
面试官:RAG 不用向量数据库,用 MySQL 硬扛?我:100 万向量不是很轻松?
mysql·面试·ai编程
weedsfly2 小时前
异步编程全景与事件循环——彻底搞懂 JS 执行机制
前端·javascript
用户059540174462 小时前
AI Agent记忆测试踩坑实录:Mock骗了我一周,Mem0+pytest一招破局
前端·css
用户1733598075372 小时前
纯前端 PDF 数字签名实战:Vue 3 + pdf-lib 在浏览器里完成签名嵌入
前端·javascript
IT_陈寒3 小时前
SpringBoot自动配置的坑,我爬了三天才出来
前端·人工智能·后端
Avan_菜菜9 小时前
AI 能写代码了,为什么我反而开始要求它先写文档?
前端·github·ai编程
爱勇宝14 小时前
鸿蒙生态的下半场:开发者不只要能开发,还要能赚钱
android·前端·程序员