模块化: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 两者都支持。
相关推荐
夏幻灵2 小时前
JavaScript this 面试:五种绑定规则、优先级与常见面试陷阱
开发语言·javascript·面试
Lvan的前端笔记2 小时前
docker:每个前端项目一个 Nginx 容器还是只有一个Nginx容器
前端·nginx·docker
三天不学习2 小时前
Egg.js 4 突然爆火,原因是否归结于AI 原生落地需求爆发?
前端·javascript·全栈·egg.js
xcs194053 小时前
前端 vue 的前端页面debugger 进不去
前端·javascript·vue.js
明月_清风4 小时前
Deno 终局来了:从挑战 Node 到被 Cloudflare 收编
前端·后端·node.js
Csvn5 小时前
框架性能优化
前端
回眸&啤酒鸭7 小时前
【回眸】OpenSwarm 多智能体协作系统实战指南
大数据·前端·人工智能
用户69371750013847 小时前
2026,程序员的时代拐点到了
android·前端·后端
大龄秃头程序员7 小时前
一次 iBeacon + BLE 无感解锁方案的实现记录
前端
小兔子7 小时前
Python 的 GIL 与 free-threading:3.13 之后「去 GIL」走到哪一步了
前端