ES6 模块(import/export)和 CommonJS(require/module.exports)的区别
ES6 模块(ESM)和 CommonJS(CJS)是 JavaScript 中两种截然不同的模块化规范。它们不仅在语法上有区别,在加载机制、导出值的形式、以及执行时机上都有着根本的不同。
以下是它们的核心区别详解:
1. 核心区别对比表(太长不看版)
| 特性 | ES6 模块 (ESM) | CommonJS (CJS) |
|---|---|---|
| 标准来源 | ECMAScript 官方语言标准 | Node.js 社区标准 |
| 语法 | import / export |
require / module.exports |
| 加载方式 | 异步加载(也支持同步),适合浏览器 | 同步加载,适合服务器端 (Node.js) |
| 解析时机 | 编译时输出(静态解析) | 运行时加载(动态解析) |
| 导出形式 | 导出的是值的动态只读引用 | 导出的是值的拷贝(浅拷贝) |
顶层 this |
undefined |
指向当前模块对象 (module.exports) |
| Tree-Shaking | 原生支持(因为是静态的) | 不支持(打包工具需额外配置或无法做到完美) |
2. 详细原理解析
① 导出值的形式:值的拷贝 vs 动态引用(最常考点)
CommonJS 输出的是值的拷贝:
一旦输出了某个值(特别是基本数据类型),模块内部发生的变化不会影响到已经输出的值。javascript// counter.js (CommonJS) let count = 1; function add() { count++; } module.exports = { count, add }; // main.js const { count, add } = require('./counter.js'); console.log(count); // 1 add(); console.log(count); // 1 (依然是1,内部改变不影响外部)ES6 Module 输出的是值的动态只读引用:
ES6 模块不会缓存运行结果,而是动态地去被加载的模块里取值。模块里面的变量绑定着所在的模块,且外部引用的变量是只读的(不能重新赋值)。javascript// counter.js (ES6) export let count = 1; export function add() { count++; } // main.js import { count, add } from './counter.js'; console.log(count); // 1 add(); console.log(count); // 2 (动态引用,值跟着变了)
② 解析时机:静态分析 vs 动态运行时
CommonJS 是动态的(运行时加载):
require本质上是一个函数,只有当代码运行到这一行时,才会去加载模块。因此,你可以把require写在if语句或者函数里,甚至使用变量作为路径。javascript// 合法 let moduleName = 'fs'; if (condition) { const fs = require(moduleName); }ES6 Module 是静态的(编译时解析):
import语句必须放在文件的最顶层,不能放在代码块(如if语句)或函数中(注:ES2020 引入的import()函数支持动态加载,但传统的import语句是静态的)。模块的依赖关系在代码编译/解析阶段就已经确定了。javascript// 报错:import 不能放在 if 内部 if (condition) { import { foo } from './foo.js'; }优势:因为是静态的,打包工具(如 Webpack、Vite、Rollup)可以很容易地进行 Tree-Shaking(摇树优化),自动剔除没有被使用到的代码,从而减小最终打包的体积。
③ 加载机制:同步 vs 异步
- CommonJS 最初为 Node.js 服务器端设计,模块文件一般存放在本地硬盘,读取速度非常快,所以采用同步加载。如果在浏览器端使用,由于网络请求较慢,同步加载会阻塞页面渲染。
- ES6 Module 充分考虑了浏览器环境,
import命令会被引擎静态分析,可以发起异步网络请求去获取模块文件,不会阻塞后续代码(虽然在 Node.js 中读取本地 ESM 文件很快,但其机制设计是兼容异步特性的)。
④ 顶层 this 的指向
- CommonJS:在模块顶层打印
this,它指向当前模块的module.exports对象。 - ES6 Module:在模块顶层打印
this,它是undefined。这是因为 ESM 默认处于严格模式 ("use strict")。
⑤ 循环依赖的处理
- CommonJS:遇到循环依赖时,只输出已经执行的部分,还未执行的部分不会输出。容易导致拿到的是不完整的对象,引发
undefined错误。 - ES6 Module:因为是动态引用,只要开发者保证真正去使用变量的时候,变量已经被初始化,就不会报错。ESM 天生能更好地处理循环依赖。
3. 如何在 Node.js 中使用它们?
Node.js 原生使用 CommonJS。如果你想在 Node.js 中使用 ES6 模块,有以下几种方法:
- 把文件后缀名改为
.mjs。 - 在项目的
package.json中添加"type": "module"字段,这样.js文件会被当做 ES6 模块处理(此时如果想用 CommonJS,需把文件后缀改成.cjs)。
4. 总结
- 如果你在写纯前端代码(Vue/React/Angular)或使用现代构建工具(Vite/Rollup),你会完全使用 ES6 模块。
- 如果你在写 Node.js 后端代码或脚本,CommonJS 依然非常常见,但现在 Node 社区也在快速向 ES6 模块 迁移。未来的趋势是 ES6 模块大一统。