ESM与CommonJS
ESM(ECMAScript Modules)和CommonJS在标准上的核心区别体现在规范制定主体和技术设计理念上,以下从社区标准与官方标准的角度对比分析:
一、标准背景差异
-
CommonJS(社区标准)
- 起源:由Mozilla工程师Kevin Dangoor于2009年发起,旨在解决服务端JS(如Node.js)的模块化问题。
- 定位:典型的社区驱动规范,Node.js核心开发者主导演进,未被纳入ECMAScript官方标准。
- 特点:功能实现偏向服务端,如
require()
同步加载模块。
-
ESM(官方标准)
- 起源:ECMA-262(ES6)标准的一部分,由TC39委员会(含浏览器厂商、Node.js代表等)制定。
- 定位:面向浏览器和服务端的语言级官方模块系统,强制静态化设计。
- 特点:通过
import
/export
语法实现,支持异步加载和Tree Shaking优化。
二、技术设计差异
1. 模块加载机制
特性 | CommonJS(社区) | ESM(官方) |
---|---|---|
加载时机 | 运行时同步加载 | 解析时静态分析,支持异步加载 |
模块解析 | 动态依赖(如require() 可条件执行) |
静态依赖(import 必须顶层) |
值绑定行为 | 导出值的拷贝(原始类型值拷贝) | 导出值的引用(动态绑定,类似指针) |
循环依赖处理 | 可能因执行顺序导致逻辑复杂 | 静态分析支持更清晰的循环引用处理 |
2. 作用域与运行时
特性 | CommonJS | ESM |
---|---|---|
顶层this |
指向module.exports ({} 默认对象) |
严格模式下为undefined |
模块作用域 | module 、exports 等隐式变量可用 |
无隐式变量,需显式使用import /export |
动态代码 | 可使用require() 动态加载模块 |
仅支持import() 动态导入 |
三、实践影响
场景 | CommonJS | ESM |
---|---|---|
浏览器支持 | 需工具(如Webpack)转为函数式包装 | 原生支持(需<script type="module"> ) |
Node.js支持 | 默认模块系统(.cjs文件) | 需.mjs 后缀或package.json 中声明 |
Tree Shaking | 难以静态分析,优化受限 | 天然支持(Webpack/Rollup等依赖此) |
元数据操作 | require.resolve() 等运行时方法可用 |
通过import.meta 实现(如import.meta.url ) |
四、标准演进趋势
- ECMAScript官方立场:ESM是未来发展方向,TC39持续强化其特性(如顶层
await
、模块的动态注册等)。 - Node.js策略:自v12起逐步完善ESM支持,但对CommonJS的兼容将持续存在。
示例:Node.js中可通过require('esm')
兼容两种规范,但推荐新项目优先使用ESM。
五、总结
维度 | CommonJS(社区) | ESM(官方) |
---|---|---|
规范主体 | 社区主导(Node.js生态) | 语言官方标准(TC39) |
核心优势 | 动态灵活,服务端友好 | 静态优化,跨平台通用 |
适用阶段 | 旧项目维护/非严格模块需求 | 新项目开发/浏览器优先场景 |
六、特性应用场景及详细解释
1. 静态分析与动态加载
-
ESM 的静态特性
ESM 通过import/export
显式声明依赖,模块的导入/导出关系可被引擎在编译阶段解析。
✔️ 应用场景:- 静态优化:Webpack/Rollup 等工具的 Tree Shaking 依赖这种静态结构剔除未使用的代码。
- 预加载:浏览器可提前预取模块资源,加速页面加载。
示例:
// ESM(编译时解析导入) import { utils } from './utils.mjs';
-
CommonJS 的动态特性
CommonJS 模块依赖通过require()
运行时解析,可在条件语句或函数中动态加载。
✔️ 应用场景:- 条件加载:根据环境或配置动态选择模块(如开发/生产环境区分)
// CommonJS(运行时决定模块路径) let modulePath; if (process.env.NODE_ENV === 'dev') { modulePath = './dev-module'; } else { modulePath = './prod-module'; } const module = require(modulePath);
2. 加载机制
-
ESM 的异步加载(浏览器)
ESM 在浏览器中默认异步加载,资源会被并行下载且不阻塞主线程。
✔️ 应用场景:- 使用
<script type="module">
时,支持async
/defer
属性优化加载顺序。 - 与动态
import()
结合实现懒加载:
// 按需加载模块(类似代码分割) button.onclick = async () => { const module = await import('./dialog.mjs'); module.openDialog(); };
- 使用
-
CommonJS 的同步加载(Node.js)
CommonJS 为服务端设计,通过同步阻塞式require()
顺序加载模块。
✖️ 局限性:- 在浏览器中需工具(如 Webpack)将模块打包为单一文件以模拟同步加载。
3. 值的引用 vs 值的拷贝
-
ESM 的实时绑定(Reference)
ESM 导出的变量是动态绑定的原始值引用,模块间的修改会互相影响。// counter.mjs export let count = 0; export function increment() { count++; } // main.mjs import { count, increment } from './counter.mjs'; console.log(count); // 0 increment(); console.log(count); // 1(同步更新)
-
CommonJS 的值拷贝
CommonJS 导出的是对象浅拷贝(类似Object.assign
),模块间的修改不会同步。// counter.js let count = 0; function increment() { count++; } module.exports = { count, increment }; // main.js const { count, increment } = require('./counter'); console.log(count); // 0 increment(); console.log(count); // 0(值未变化)
4. 循环依赖的解决
-
ESM 的引用传递
即使模块 A 和 B 互相依赖,导出的值会实时更新(通过绑定机制)。// a.mjs import { b } from './b.mjs'; export const a = 'a'; console.log(b); // 'b'(此处正常) // b.mjs import { a } from './a.mjs'; export const b = 'b'; console.log(a); // undefined(此时a未初始化完成)
-
CommonJS 的时序依赖
循环依赖可能导致部分导出未完成。// a.js const b = require('./b'); exports.a = 'a'; // 导出的a在此处赋值 // b.js const a = require('./a'); console.log(a.a); // undefined(因为a.js的exports暂未赋值完成) exports.b = 'b';
七、实践建议
- 前端工程:优先 ESM
与现代框架(React/Vue)和构建工具完美适配,利用 Tree Shaking 优化体积。 - Node.js 服务端
新项目建议使用 ESM(文件后缀.mjs
或package.json
设置"type": "module"
),旧项目沿用 CommonJS。 - 混合兼容方案
使用esm
库或 Babel/Webpack 转换兼容旧环境。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Blazor Hybrid适配到HarmonyOS系统
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 解决跨域问题的这6种方案,真香!
· 分享4款.NET开源、免费、实用的商城系统
· 一套基于 Material Design 规范实现的 Blazor 和 Razor 通用组件库