CommonJS 与 ES Module:从语法到机制的全面对比

15 阅读4分钟

在前端与 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';

区别总结:

特性CommonJSES Module
导出方式module.exports / exportsexport / 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 中,顶层 thisundefined,而不是 windowglobalThis

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 官方标准

特性CommonJSES 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 ShakingES Module(静态结构)
动态加载需求CommonJS(同步)或 import()(异步)

九、结语:ESM 是未来,但 CMJ 仍在

CommonJS 是 Node.js 的历史遗产,它的同步加载和动态特性在过去十年中支撑了庞大的生态。而 ES Module 作为官方标准,带来了更好的静态分析、异步加载和跨平台支持。

未来属于 ESM,但在过渡期内,理解两者的差异、掌握互操作技巧,是每一位 JavaScript 开发者的必修课。


如需进一步了解如何在 Node.js 中启用 ESM(如 "type": "module".mjs 扩展名),或如何在构建工具(如 Webpack、Vite)中配置模块解析,欢迎留言交流。


参考资料: