模块化: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 文件中不能使用 require、module、exports、__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 两者都支持。
相关推荐
椒哥1 小时前
一天被问几十次"允许",我统计了 517 次,然后写了个插件
前端·javascript·github
风骏时光牛马1 小时前
生成一个AI稳定性治理及疑难问题分析标题,只需要一个标题,不需要其他的任何文本。
前端
莫西干式秃头2 小时前
用 AI + threejs 写了个浏览器里的 3D 动作游戏《武来》:砍怪、放技能、开 SU7
前端·人工智能·three.js
kyriewen2 小时前
我测了 6 种防请求竞态的写法:只有 1 种真能打
前端·javascript·程序员
孟健2 小时前
用 WorkBuddy 做个人名片网页,挂上开源设计技能后效果完全不同了
前端·人工智能·ai编程
IT_陈寒2 小时前
SpringBoot自动配置的坑,这次终于爬出来了
前端·人工智能·后端
LuHang3 小时前
从天地图切换到高德地图:一次真实的底图迁移实战
前端·vue.js·cesium
用户360055579003 小时前
Kestrel_VLLM - 27 · 寄存器常驻与融合发射:两次「少发一次」
前端
前端小万3 小时前
用"道法术器"重构你和 AI 的协作方式
前端·人工智能·程序员
鶴哥只手遮天3 小时前
Angular 22 源码级深度解析-第 06 章 模板指令全解:`ɵɵelementStart`、`ɵɵproperty`、`ɵɵadvance` 与绑定指令
前端·开源·angular.js