ES Modules 与 CommonJS 的核心区别
ES Modules (ESM) 和 CommonJS (CJS) 是 JavaScript 中两种主要的模块系统,它们在语法、加载机制、作用域和适用场景等方面存在显著差异。
1. 语法差异
ES Modules (ESM)
- 使用
import
和export
关键字。 - 静态导入/导出(编译时确定依赖关系)。
javascript
// 导出
export const name = 'John';
export function greet() { /* ... */ }
export default class MyClass { /* ... */ }
// 导入
import { name, greet } from './module.js';
import MyClass from './module.js';
import * as module from './module.js';
CommonJS (CJS)
- 使用
require()
和module.exports
/exports
。 - 动态导入(运行时确定依赖关系)。
javascript
// 导出
module.exports = {
name: 'John',
greet: function() { /* ... */ }
};
// 或
exports.name = 'John';
exports.greet = function() { /* ... */ };
// 导入
const module = require('./module.js');
const { name, greet } = require('./module.js');
2. 加载机制
ESM
- 静态分析:依赖关系在编译时确定,支持 Tree Shaking(移除未使用代码)。
- 异步加载:模块文件通过网络请求异步获取,但执行时是同步的(按依赖顺序)。
- 浏览器支持 :通过
<script type="module">
标签引入。
CJS
- 动态加载:依赖关系在运行时确定,不支持 Tree Shaking。
- 同步加载 :
require()
是同步操作,适合 Node.js 环境(文件系统读取快)。 - Node.js 原生支持 :默认模块格式(
.js
文件)。
3. 作用域与引用类型
ESM
- 静态只读视图:导入的是导出值的只读引用,原始值变化会反映到导入端。
javascript
// 导出模块
export let counter = 0;
export function increment() { counter++; }
// 导入模块
import { counter, increment } from './module.js';
console.log(counter); // 0
increment();
console.log(counter); // 1(跟随原始值变化)
CJS
- 值的拷贝:导入的是导出值的副本,原始值变化不会影响导入端。
javascript
// 导出模块
let counter = 0;
module.exports = {
counter,
increment: function() { counter++; }
};
// 导入模块
const { counter, increment } = require('./module.js');
console.log(counter); // 0
increment();
console.log(counter); // 0(保持副本值)
4. 文件扩展名与环境支持
ESM
- 浏览器 :直接支持,需使用
<script type="module">
。 - Node.js :
.mjs
文件默认启用 ESM。.js
文件需在package.json
中添加"type": "module"
。- 导入
.cjs
文件需显式指定路径(如require('./file.cjs')
)。
CJS
- 浏览器:需通过打包工具(如 Webpack)转换。
- Node.js :
.js
文件默认使用 CJS。.cjs
文件强制使用 CJS(即使"type": "module"
)。
5. 顶层 this
指向
ESM
- 顶层
this
为undefined
(严格模式)。
CJS
- 顶层
this
指向module.exports
。
javascript
// ESM
console.log(this); // undefined
// CJS
console.log(this === module.exports); // true
6. 动态导入支持
ESM
-
支持动态导入(返回 Promise):
javascriptimport('./module.js').then((module) => { // 使用 module });
CJS
- 仅支持同步导入,但可通过
require('module/path')
动态指定路径(运行时解析)。
7. 循环依赖处理
ESM
- 支持循环依赖,导入时可能获取未完全初始化的值(需谨慎处理)。
CJS
- 循环依赖时,已加载模块返回当前状态的副本,可能导致值不完整。
8. 适用场景
ESM
- 现代浏览器应用。
- Node.js 新应用(需处理兼容性)。
- 需要 Tree Shaking 的库。
CJS
- Node.js 传统应用。
- 不需要 Tree Shaking 的工具库。
- 需要动态加载的场景。
总结对比表
特性 | ES Modules (ESM) | CommonJS (CJS) |
---|---|---|
语法 | import/export |
require/module.exports |
加载时机 | 静态(编译时) | 动态(运行时) |
依赖类型 | 只读引用 | 值的拷贝 |
顶层 this |
undefined |
module.exports |
Tree Shaking | 支持 | 不支持 |
异步加载 | 支持 | 不支持 |
文件扩展名 | .mjs (Node.js) |
.js (默认)、.cjs |
浏览器支持 | 原生支持 | 需打包 |
如何选择?
- 新项目:优先使用 ESM,享受静态分析和 Tree Shaking 的优势。
- Node.js 兼容性:若需兼容旧版 Node.js(<14.x),使用 CJS。
- 混合使用 :通过
package.json
配置"type": "module"
,并为 CJS 文件使用.cjs
扩展名。
在实际开发中,理解两者差异有助于避免模块加载错误和性能问题,尤其是在构建大型应用或库时。