在编写 JavaScript 代码层面,.mjs
(代表 ESM,ES 模块)和 .cjs
(代表 CommonJS 模块)的区别不仅仅体现在引入模块使用 import
还是 require
上,下面从多个方面详细介绍它们的差异:
1. 模块导入和导出语法
-
导入语法
-
.mjs
(ESM):使用import
关键字,支持静态导入和动态导入。静态导入在文件顶部进行,动态导入可以在代码的任何位置按需加载模块。
-
// 静态导入
import { someFunction } from './module.mjs';
// 动态导入
const module = await import('./module.mjs');
-
.cjs
(CommonJS):使用require
函数进行导入,它是同步的,通常在文件顶部调用。
const { someFunction } = require('./module.cjs');
-
导出语法
-
.mjs
(ESM):使用export
关键字,可以有命名导出和默认导出。
-
// 命名导出
export const someValue = 42;
export function someFunction() {}
// 默认导出 函数名可选,一般直接function(){}
export default function defaultFunction() {}
-
.cjs
(CommonJS):使用module.exports
或exports
对象进行导出。
// 命名导出
exports.someValue = 42;
exports.someFunction = function() {};
// 默认导出
module.exports = function defaultFunction() {};
2. 静态 vs 动态分析
-
.mjs
(ESM):支持静态分析,因为import
和export
语句在编译时就可以确定模块之间的依赖关系。这使得工具(如打包工具、静态代码分析工具)可以更高效地进行优化,例如进行 Tree Shaking(移除未使用的代码)。 -
.cjs
(CommonJS):require
是动态的,在运行时才会解析模块路径。这使得静态分析变得困难,无法进行有效的 Tree Shaking。
3. 作用域和执行顺序
-
.mjs
(ESM):模块作用域是静态的,模块在被导入时会先进行静态分析,然后按照拓扑顺序执行。一个模块只会被执行一次,后续的导入会复用之前的执行结果。 -
.cjs
(CommonJS):模块作用域是动态的,每次调用require
时都会执行模块代码(如果模块还未被缓存)。模块的执行顺序取决于require
调用的顺序。
4. 文件扩展名和环境支持
-
.mjs
(ESM):在 Node.js 中,使用.mjs
扩展名明确表示该文件是 ES 模块。在浏览器中,ES 模块可以通过<script type="module">
标签引入。 -
.cjs
(CommonJS):在 Node.js 中,.js
文件默认是 CommonJS 模块,也可以使用.cjs
扩展名来明确标识。浏览器不原生支持 CommonJS 模块,需要通过打包工具(如 Webpack、Browserify)进行转换。
5. 全局变量和模块上下文
-
.mjs
(ESM):没有require
、exports
、module.exports
这些 CommonJS 特定的全局变量。在浏览器中,ES 模块的顶级作用域不是全局作用域,this
值为undefined
。 -
.cjs
(CommonJS):有require
、exports
、module.exports
等全局变量,this
指向module.exports
。
6. 异步加载支持
-
.mjs
(ESM):支持动态导入,这使得模块可以异步加载,适合处理按需加载的场景,如懒加载组件。
async function loadModule() {
const module = await import('./module.mjs');
module.someFunction();
}
-
.cjs
(CommonJS):require
是同步的,不支持直接的异步加载。虽然可以通过一些技巧(如使用child_process
或第三方库)实现异步加载模块,但相对复杂。