ES Module(ESM,ES6 模块系统)和 CommonJS 是 JavaScript 中两种主流的模块规范,分别用于现代前端和 Node.js 环境(早期),它们在语法、加载机制、特性等方面有显著区别。以下是详细对比:
一、语法差异
1. 导出(Export)
ES Module:
使用 export 关键字,支持命名导出和默认导出,可在同一模块中混合使用。
运行
// 命名导出(多个)
export const name = 'foo';
export function func() {};// 默认导出(一个模块只能有一个)
export default { id: 1 };
CommonJS:
使用 module.exports 或 exports 导出,本质是导出一个对象(默认导出)。
运行
// 导出对象
module.exports = {name: 'foo',func: function() {}
};// 也可单独赋值(通过 exports 引用 module.exports)
exports.name = 'foo';
exports.func = function() {};
2. 导入(Import)
ES Module:
使用 import 关键字,支持导入命名成员、默认成员或整体导入。
运行
// 导入命名成员
import { name, func } from './module.js';// 导入默认成员(可自定义名称)
import obj from './module.js';// 整体导入(命名空间对象)
import * as mod from './module.js';
CommonJS:
使用 require() 函数导入,返回模块导出的对象。
运行
// 整体导入
const mod = require('./module.js');
console.log(mod.name); // 访问导出的成员// 解构导入(模拟命名导入)
const { name, func } = require('./module.js');
二、加载机制
1. 加载时机
ES Module:
- 静态加载:导入和导出语句在代码解析阶段(编译时)执行,而非运行时。
- 导入语句只能放在模块顶层(不能在 if、函数等代码块中),浏览器 / 引擎可提前分析模块依赖,实现树摇(Tree Shaking)(删除未使用的代码)。
CommonJS:
- 动态加载:require() 在运行时执行,可根据条件动态导入(如在 if 语句中调用 require())。
- 无法在编译时确定依赖关系,不支持树摇。
2. 模块加载方式
ES Module:
- 异步加载:在浏览器中,ESM 通过
运行
// module.js
export let count = 0;
export function increment() { count++; }// main.js
import { count, increment } from './module.js';
increment();
console.log(count); // 输出 1(同步更新)
CommonJS:
- 同步加载:在 Node.js 中,require() 是同步加载,适合服务端(文件读取快),不适合浏览器(会阻塞渲染)。
- 值的拷贝:导入的是模块导出值的 “拷贝”,模块内部后续修改不会影响导入方。
运行
// module.js
let count = 0;
module.exports = {count,increment() { count++; }
};// main.js
const mod = require('./module.js');
mod.increment();
console.log(mod.count); // 输出 0(拷贝未更新)
三、模块标识与路径
ES Module:
- 必须使用完整路径(包括文件扩展名,如 .js、.mjs),或通过配置(如 package.json 的 type: “module”)省略。
- 支持绝对路径、相对路径和 URL(如
import 'https://cdn.example.com/module.js'
)。
运行
import './utils.js'; // 必须带 .js 扩展名(Node.js 中需配置)
CommonJS:
- 可省略文件扩展名(.js、.json 等会被自动补全),支持查找 node_modules 中的模块。
运行
require('./utils'); // 自动补全为 ./utils.js
require('lodash'); // 从 node_modules 中查找
四、循环依赖处理
ES Module:
- 遇到循环依赖(A 依赖 B,B 依赖 A)时,会返回 “未完成的模块实例”(部分导出已可用),后续代码执行时补充完整。
运行
// a.js
import { b } from './b.js';
export const a = 1;
console.log('a 中 b 的值:', b); // 输出 undefined(此时 b 尚未导出)// b.js
import { a } from './a.js';
export const b = 2;
console.log('b 中 a 的值:', a); // 输出 1(a 已导出)
CommonJS:
- 循环依赖时,会缓存已执行的部分模块,返回 “缓存的导出对象”(可能不完整)。
运行
// a.js
const { b } = require('./b.js');
exports.a = 1;
console.log('a 中 b 的值:', b); // 输出 undefined(b 尚未导出)// b.js
const { a } = require('./a.js');
exports.b = 2;
console.log('b 中 a 的值:', a); // 输出 undefined(a 尚未导出)
五、适用环境
ES Module:
- 现代浏览器(原生支持,需·
<script type="module">
)。
Node.js 14.3+(需文件后缀为 .mjs 或 package.json 中设置 “type”: “module”)。 - 前端工程化工具(Webpack、Vite 等)默认支持,是前端模块化的主流方案。
CommonJS:
- 主要用于 Node.js 环境(默认模块系统),早期前端通过 Browserify、Webpack 等工具转换后使用。
- 目前 Node.js 仍广泛兼容,但新项目更推荐 ESM。
六、核心区别总结
七、如何选择?
- 前端项目:优先使用 ES Module,配合工程化工具实现树摇和优化。
- Node.js 项目:新项目推荐 ES Module(“type”: “module”),旧项目兼容 CommonJS。
- 需动态加载模块(如根据条件导入):可在 ESM 中使用 import() 函数(返回 Promise,支持动态加载),兼具静态分析和动态能力。