在JavaScript的世界中,模块化是构建大型应用的关键。ES模块(ESM)和CommonJS是两种主流的模块系统,它们各自有着不同的特性和使用场景。你了解它们的区别吗?
ES模块 (ESM)
ES模块是 ECMAScript 官方标准的一部分,它使用import
和export
语句来导入和导出模块。ES 模块是 JavaScript 官方标准的一部分,已被现代浏览器和 JavaScript 运行时(如Node.js)所支持。
主要特点
- 静态结构:ES模块在编译时就确定了模块的依赖关系,可以进行静态分析。
- 模块作用域:每个模块都有自己的作用域,默认情况下,模块中的变量不会泄漏到全局作用域。
- 异步加载:在浏览器环境中,ES模块可以异步加载,有助于提高性能。
模块解析
- ES模块的文件扩展名通常是
.js
或.mjs
,其中.mjs
明确表示该文件是一个ES模块。 - 在Node.js中,你需要在
package.json
中设置"type": "module"
来启用ES模块支持。
代码示例
// module.js
export function foo() {
console.log("foo");
}
export const bar = () => {
console.log("bar");
}
// main.js
import { foo, bar } from './module.js';
foo(); // 输出 "foo"
bar(); // 输出 "bar"
CommonJS模块
CommonJS模块是Node.js默认的模块系统,它使用require
和module.exports
来导入和导出模块。
主要特点
- 动态结构:CommonJS模块在运行时确定模块的依赖关系,不能进行静态分析。
- 同步加载:在Node.js中,CommonJS模块是同步加载的。
- 广泛使用:由于Node.js默认采用CommonJS模块系统,所以在Node.js生态系统中广泛使用。
// module.js
function foo() {
console.log("foo");
}
module.exports = { foo };
// main.js
const { foo } = require('./module.js');
foo(); // 输出 "foo"
缓存机制
CommonJS模块在第一次加载时会被缓存,后续加载同一个模块时会返回缓存的版本。这有助于提高性能,但也意味着模块的初始化代码只会执行一次。
// module.js
console.log("Module loaded");
module.exports = { foo: "bar" };
// main.js
const module1 = require('./module.js');
const module2 = require('./module.js');
// 只输出一次 "Module loaded"
循环依赖
CommonJS模块支持循环依赖,但需要注意的是,循环依赖可能会导致模块加载顺序和结果不确定。
// a.js
const b = require('./b.js');
console.log('a.js');
module.exports = { name: 'module a' };
// b.js
const a = require('./a.js');
console.log('b.js');
module.exports = { name: 'module b' };
// main.js
require('./a.js');
require('./b.js');
// 输出顺序: a.js -> b.js
区别
主要的区别如下表所示:
特性 | ES模块 (ESM) | CommonJS模块 |
---|---|---|
语法 | import / export | require / module.exports |
加载方式 | 异步 (浏览器) | 同步 |
静态分析 | 支持,有利于Tree Shaking | 不支持,难以优化死代码 |
Tree Shaking | 自然支持,提升代码体积优化 | 部分支持(需工具辅助,效果有限) |
Top-Level Await | 支持,便于编写异步初始化代码 | 不支持,需包裹在async函数内 |
作用域 | 模块作用域,提升封装性 | 文件作用域,可能导致污染全局 |
使用场景 | 浏览器和现代Node.js (>=13.2, 需配置) | 传统Node.js及大量现有生态 |
模块循环依赖 | 处理更为严格,可能导致错误 | 更宽松,但可能隐藏逻辑问题 |
Tree Shaking 是什么?
- ES模块自然支持Tree Shaking(枯树摇动),这是一种在打包过程中移除未被引用的代码的技术,有助于减小最终打包文件的体积。这是因为ES模块的静态结构使得编译器能够明确地知道哪些导出没有被使用。
- CommonJS模块不直接支持Tree Shaking,因为它们是动态加载的。虽然一些现代打包工具如Webpack通过静态分析尝试实现类似Tree Shaking的效果,但可能不如ES模块那样彻底和高效。
Top-Level Await 是什么?
ES模块支持在模块顶层使用await
关键字,咱们可以在脚本的最外层直接使用await
关键字,等待Promise解析,而不需要将代码封装在async函数内。这在处理异步初始化或者配置加载等场景特别有用。
// example.mjs
console.log('Start');
const response = await fetch('https://juejin.cn/user/2049145406229127');
const data = await response.json();
console.log('Data received:', data);
console.log('End');
CommonJS模块不支持顶层await,在CommonJS模块中,咱们不能在模块的顶级作用域直接使用await
。如果非要这样做,那就会遇到语法错误。咱们需要将await
放在一个async function
内部。
// example.js
console.log('Start');
(async () => {
const response = await fetch('https://juejin.cn/user/2049145406229127');
const data = await response.json();
console.log('Data received:', data);
})();
console.log('End');
兼容性
在Node.js中使用ES模块
如果咱们在Node.js项目中使用ES模块,确保在 package.json
中设置 "type": "module"
,或者使用 .mjs
文件扩展名。
{
"type": "module"
}
在ES模块中使用CommonJS模块
咱们可以在ES模块中使用 import
语句来导入CommonJS模块,但需要注意模块的默认导出和命名导出的区别。
// commonjs-module.js
module.exports = { foo: "bar" };
// es-module.js
import commonjsModule from './commonjs-module.js';
console.log(commonjsModule.foo); // 输出 "bar"
在CommonJS模块中使用ES模块
咱们可以在CommonJS模块中使用 import
语句,但需要使用动态导入语法
// es-module.js
export const foo = "bar";
// commonjs-module.js
(async () => {
const esModule = await import('./es-module.js');
console.log(esModule.foo); // 输出 "bar"
})();
选择建议
- 浏览器环境:现代JavaScript开发,优先使用ES模块,因为它们支持支持静态分析和异步加载,有助于提高性能,推荐在浏览器和现代Node.js项目中使用。
- Node.js项目:- CommonJS模块广泛应用于Node.js生态系统,支持同步加载和动态依赖,更适合传统的Node.js项目。
注意一下,迁移现有项目从CommonJS到ES模块可能需要一定的工作量,包括修改导入导出语句、处理循环依赖问题,以及确保依赖库也支持ES模块