在前端与 Node.js 开发中,模块化是组织代码的核心手段。随着 JavaScript 生态的发展,模块系统也经历了从 CommonJS(CMJ) 到 ES Module(ESM) 的演进。尽管两者都能实现模块化,但它们在语法、加载机制、标准来源、运行行为等方面存在本质区别。
本文将从多个维度深入对比 CommonJS 与 ES Module,帮助你理解它们的差异,并在实际开发中做出正确选择。
一、语法差异:require/module.exports vs import/export
最直观的区别体现在语法上。
CommonJS 语法
// 导出
module.exports = {
foo: function() {
console.log('foo');
}
};
// 导入
const moduleA = require('./moduleA');
ES Module 语法
// 导出
export function foo() {
console.log('foo');
}
// 导入
import { foo } from './moduleA.js';
区别总结:
特性 | CommonJS | ES Module |
---|---|---|
导出方式 | module.exports / exports | export / export default |
导入方式 | require() | import |
是否支持默认导出 | 是(通过 module.exports ) | 是(export default ) |
是否支持命名导出 | 否(需手动构造对象) | 是(export const foo = ... ) |
二、模块加载机制:同步 vs 异步
CommonJS:同步加载
CommonJS 是 同步加载 的模块系统,模块在运行时立即加载并执行。
const fs = require('fs');
const data = fs.readFileSync('./file.txt');
这种机制在 服务器端(如 Node.js) 是合理的,因为文件通常本地存在,读取速度快。
ES Module:异步加载
ES Module 支持 异步加载,模块在编译阶段就确定了依赖关系,运行时按需加载。
import('./moduleA.js').then(module => {
module.foo();
});
这使得 ESM 更适合 浏览器环境,可以避免阻塞主线程。
三、this 指向:空对象 vs window
CommonJS 中的 this
在 CommonJS 模块中,this
指向当前模块的 空对象 {}
,而不是全局对象。
console.log(this); // {}
ES Module 中的 this
在 ESM 中,顶层 this
是 undefined
,而不是 window
或 globalThis
。
console.log(this); // undefined
这有助于避免无意中污染全局作用域,是 ESM 更加“严格”的体现。
四、循环依赖的处理机制
CommonJS 的循环依赖
CommonJS 在运行时才确定模块依赖,遇到循环依赖时,已执行部分会被缓存,未执行部分为 undefined
。
// a.js
const b = require('./b');
console.log('a:', b);
module.exports = 'a';
// b.js
const a = require('./a');
console.log('b:', a);
module.exports = 'b';
输出:
b: {}
a: b
ES Module 的循环依赖
ESM 在编译阶段就确定依赖关系,遇到循环依赖时,会建立绑定关系,即使模块未执行完毕,也能访问已导出的绑定。
// a.mjs
import { b } from './b.mjs';
console.log('a:', b);
export const a = 'a';
// b.mjs
import { a } from './a.mjs';
console.log('b:', a);
export const b = 'b';
输出:
b: undefined
a: b
虽然 a
还未初始化,但 ESM 会建立“ live binding”,后续值会动态更新。
五、标准来源:社区标准 vs 官方标准
特性 | CommonJS | ES Module |
---|---|---|
标准来源 | 社区标准(2009 年由 Mozilla 工程师提出) | ECMAScript 官方标准(ES6 引入) |
兼容性 | Node.js 原生支持 | 现代浏览器 + Node.js(v12+)支持 |
发展方向 | 逐步被 ESM 替代 | 未来主流标准 |
六、运行时 vs 编译时:动态 vs 静态
CommonJS:运行时加载
- 模块路径可以是变量:
require(path)
- 条件加载:
if (condition) require('./a')
- 动态性高,但难以优化
ES Module:编译时静态分析
- 模块路径必须是字符串字面量:
import ... from 'module'
- 不支持条件导入(顶层)
- 支持静态分析,便于 Tree Shaking 和 作用域优化
七、互操作性:CMJ 与 ESM 如何共存?
在实际项目中,CommonJS 与 ESM 模块可能共存,但互操作存在限制:
在 ESM 中导入 CMJ
import pkg from 'commonjs-package'; // 默认导入
注意:CMJ 模块只能作为 默认导出,无法解构命名导出。
在 CMJ 中导入 ESM
import('esm-package').then(module => {
module.foo();
});
必须使用 动态 import(),不能直接使用
require()
。
八、实战建议:如何选择?
场景 | 推荐模块系统 |
---|---|
Node.js 旧项目 | CommonJS(兼容性好) |
新项目(Node.js + 前端) | ES Module(未来标准) |
浏览器环境 | ES Module(原生支持) |
需要 Tree Shaking | ES Module(静态结构) |
动态加载需求 | CommonJS(同步)或 import()(异步) |
九、结语:ESM 是未来,但 CMJ 仍在
CommonJS 是 Node.js 的历史遗产,它的同步加载和动态特性在过去十年中支撑了庞大的生态。而 ES Module 作为官方标准,带来了更好的静态分析、异步加载和跨平台支持。
未来属于 ESM,但在过渡期内,理解两者的差异、掌握互操作技巧,是每一位 JavaScript 开发者的必修课。
如需进一步了解如何在 Node.js 中启用 ESM(如 "type": "module"
或 .mjs
扩展名),或如何在构建工具(如 Webpack、Vite)中配置模块解析,欢迎留言交流。
参考资料: