ES6 模块与 CommonJS 的区别详解
文章目录
- [ES6 模块与 CommonJS 的区别详解](#ES6 模块与 CommonJS 的区别详解)
-
- 二、核心差异详解
-
- [1. 语法差异](#1. 语法差异)
-
- [CommonJS 语法](#CommonJS 语法)
- [ES6 模块语法](#ES6 模块语法)
- [2. 加载时机](#2. 加载时机)
-
- CommonJS:运行时动态加载
- [ES6 模块:编译时静态加载](#ES6 模块:编译时静态加载)
- [3. 输出机制](#3. 输出机制)
-
- CommonJS:值的拷贝
- [ES6 模块:值的引用](#ES6 模块:值的引用)
- [4. this 指向](#4. this 指向)
-
- [CommonJS:this 指向 module.exports 对象](#CommonJS:this 指向 module.exports 对象)
- [ES6 模块:this 指向 undefined](#ES6 模块:this 指向 undefined)
- [5. 循环依赖处理](#5. 循环依赖处理)
-
- CommonJS:循环依赖采用部分加载
- [ES6 模块:循环依赖采用动态绑定](#ES6 模块:循环依赖采用动态绑定)
- [6. 在 Node.js 中的使用](#6. 在 Node.js 中的使用)
-
- CommonJS:默认支持,无需额外配置
- [ES6 模块:两种启用方式](#ES6 模块:两种启用方式)
- [7. 动态导入](#7. 动态导入)
-
- CommonJS:天然支持动态导入
- [ES6 模块:通过 `import()` 实现](#ES6 模块:通过
import()实现)
- 三、总结对比(详细对比表)
- 四、结尾:适用场景与发展趋势
-
- [1. 适用场景](#1. 适用场景)
- [2. 发展趋势](#2. 发展趋势)
在 JavaScript 模块化发展的历程中, ES6 模块(ESM) 与 CommonJS(CJS) 是两种极具影响力的模块规范。它们的核心目标都是解决大型项目中代码冗余、作用域混乱、依赖管理复杂等问题,让代码组织更具规范性、可维护性和可复用性。但由于设计理念、诞生背景的不同,二者在语法、加载机制、输出特性等多个维度存在显著差异。本文将从多方面展开详细对比,帮助你清晰理解二者的区别与适用场景。
二、核心差异详解
1. 语法差异
模块规范的核心语法围绕「导入」和「导出」展开,二者的语法风格差异明显,且 CommonJS 更偏向简洁的命令式语法,ES6 模块则采用声明式语法。
CommonJS 语法
CommonJS 使用 require() 函数进行模块导入,通过 module.exports 或 exports 进行模块导出,语法简洁直观。
javascript
// 导出模块:utils.js
// 方式1:整体导出(推荐,语义更清晰)
function sum(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = {
sum,
multiply
};
// 方式2:单个导出(exports 是 module.exports 的引用,不可直接赋值)
exports.subtract = function(a, b) {
return a - b;
};
// 导入模块:main.js
const { sum, multiply, subtract } = require('./utils.js');
console.log(sum(2, 3)); // 输出:5
console.log(multiply(2, 3)); // 输出:6
console.log(subtract(3, 2)); // 输出:1
ES6 模块语法
ES6 模块使用 export 关键字进行导出,使用 import 关键字进行导入,支持命名导出和默认导出两种形式,语法更严谨规范。
javascript
// 导出模块:utils.js
// 方式1:命名导出(可多个,导入时需与导出名称一致)
export function sum(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// 方式2:默认导出(仅一个,导入时可自定义名称)
export default function subtract(a, b) {
return a - b;
}
// 导入模块:main.js
// 导入命名导出
import { sum, multiply } from './utils.js';
// 导入默认导出
import subtract from './utils.js';
console.log(sum(2, 3)); // 输出:5
console.log(multiply(2, 3)); // 输出:6
console.log(subtract(3, 2)); // 输出:1
2. 加载时机
加载时机是二者的核心差异之一,直接决定了模块的使用场景和性能表现。
CommonJS:运行时动态加载
CommonJS 模块的加载发生在代码运行阶段 ,而非编译阶段。这意味着 require() 函数可以在代码的任意位置调用,支持条件判断、变量拼接等动态加载逻辑,只有当代码执行到 require() 语句时,才会去加载对应的模块并执行其代码。
javascript
// main.js:CommonJS 动态加载示例
const env = 'production';
let utils;
// 支持条件加载
if (env === 'production') {
utils = require('./utils.prod.js');
} else {
utils = require('./utils.dev.js');
}
console.log(utils.sum(2, 3));
ES6 模块:编译时静态加载
ES6 模块的加载发生在代码编译阶段 (即预处理阶段),在代码运行前就已经完成模块的解析和依赖收集。这意味着 import 语句必须放在模块的顶层作用域,不能嵌套在 if、for 等代码块中,不支持动态的条件加载逻辑。
javascript
// main.js:ES6 模块静态加载示例
// 正确:import 放在顶层作用域
import { sum } from './utils.js';
console.log(sum(2, 3));
// 错误:import 不能嵌套在代码块中(编译阶段会报错)
// const env = 'production';
// if (env === 'production') {
// import { sum } from './utils.prod.js';
// }
3. 输出机制
输出机制的差异决定了「模块导出的值与原始模块内部值的关联关系」,二者分别采用「值的拷贝」和「值的引用」两种机制。
CommonJS:值的拷贝
CommonJS 模块导出的是值的浅拷贝 ,当模块被 require() 加载时,会将导出对象(module.exports)中的值拷贝一份到导入模块中。后续原始模块内部的变量值发生变化,不会影响已经导入的拷贝值(引用类型拷贝的是引用地址,内部属性变化会同步,变量本身的重新赋值不会同步)。
javascript
// counter.js:CommonJS 模块
let count = 0;
function increment() {
count++;
}
module.exports = {
count, // 拷贝当前 count 的值(0)
increment
};
// main.js:导入 CommonJS 模块
const { count, increment } = require('./counter.js');
console.log(count); // 输出:0(初始拷贝值)
increment(); // 调用方法修改 counter.js 内部的 count(变为 1)
console.log(count); // 输出:0(拷贝值不会更新,仍为初始的 0)
ES6 模块:值的引用
ES6 模块导出的是值的只读引用,导入模块获取的不是值的拷贝,而是对原始模块内部变量的引用。原始模块内部的变量值发生变化时,导入模块中的引用值会实时同步更新(同时导入的引用是只读的,不能在导入模块中修改原始模块的变量)。
javascript
// counter.js:ES6 模块
export let count = 0;
export function increment() {
count++;
}
// main.js:导入 ES6 模块
import { count, increment } from './counter.js';
console.log(count); // 输出:0(引用原始模块的 count)
increment(); // 调用方法修改原始模块的 count(变为 1)
console.log(count); // 输出:1(引用值实时更新)
// 错误:导入的引用是只读的,不能直接修改
// count = 10;
4. this 指向
二者模块顶层的 this 指向存在本质区别,这也是判断模块类型的一个重要标识。
CommonJS:this 指向 module.exports 对象
在 CommonJS 模块中,顶层作用域的 this 指向当前模块的 module.exports 对象,通过 this 可以直接修改或添加导出内容。
javascript
// utils.js:CommonJS 模块
console.log(this); // 输出:{}(初始为空的 module.exports 对象)
console.log(this === module.exports); // 输出:true
this.hello = function() {
return 'Hello CommonJS';
};
// main.js
const { hello } = require('./utils.js');
console.log(hello()); // 输出:Hello CommonJS
ES6 模块:this 指向 undefined
ES6 模块采用严格模式(即使未显式声明 'use strict'),顶层作用域的 this 指向 undefined,无法通过 this 访问或修改模块导出内容。
javascript
// utils.js:ES6 模块
console.log(this); // 输出:undefined
// 尝试通过 this 添加导出内容(无效)
this.hello = function() {
return 'Hello ES6 Module';
};
// main.js
import { hello } from './utils.js';
console.log(hello()); // 报错:hello is not a function
5. 循环依赖处理
循环依赖指两个或多个模块互相导入对方(如 A 导入 B,B 又导入 A),二者对循环依赖的处理机制不同,导致最终的执行结果也存在差异。
CommonJS:循环依赖采用部分加载
CommonJS 处理循环依赖时,采用「部分加载」机制:当模块发生循环依赖时,被依赖的模块不会等待完全执行完毕,而是将当前已经执行完成的部分导出内容(module.exports 已赋值的部分)进行拷贝,返回给导入模块,后续未执行的内容不会被同步。
javascript
// a.js:CommonJS 模块,导入 b.js
const b = require('./b.js');
console.log('a.js 中的 b:', b);
let aValue = 'a 原始值';
module.exports = {
aValue,
updateA: function(newValue) {
aValue = newValue;
}
};
// b.js:CommonJS 模块,导入 a.js
const a = require('./a.js');
console.log('b.js 中的 a:', a); // 输出:{ aValue: undefined, updateA: [Function] }(部分加载,aValue 尚未赋值)
module.exports = {
bValue: 'b 原始值',
updateB: function(newValue) {
this.bValue = newValue;
}
};
// main.js:导入 a.js 触发循环依赖
const a = require('./a.js');
console.log('main.js 中的 a:', a); // 输出:{ aValue: 'a 原始值', updateA: [Function] }
ES6 模块:循环依赖采用动态绑定
ES6 模块处理循环依赖时,采用「动态绑定」机制:由于 ES6 模块导出的是值的引用,而非拷贝,即使发生循环依赖,导入模块获取的也是原始模块的实时引用,当原始模块后续执行完成并更新变量值时,导入模块的引用值会同步更新,无需等待模块完全执行完毕。
javascript
// a.js:ES6 模块,导入 b.js
import { bValue, updateB } from './b.js';
console.log('a.js 中的 bValue:', bValue); // 输出:b 原始值(动态绑定,获取实时值)
export let aValue = 'a 原始值';
export function updateA(newValue) {
aValue = newValue;
}
// b.js:ES6 模块,导入 a.js
import { aValue, updateA } from './a.js';
console.log('b.js 中的 aValue:', aValue); // 输出:undefined(此时 a.js 尚未赋值 aValue)
export let bValue = 'b 原始值';
export function updateB(newValue) {
bValue = newValue;
}
// main.js:导入 a.js 触发循环依赖
import { aValue, updateA } from './a.js';
console.log('main.js 中的 aValue:', aValue); // 输出:a 原始值
updateA('a 更新值');
console.log('main.js 中的 aValue:', aValue); // 输出:a 更新值
6. 在 Node.js 中的使用
Node.js 作为服务器端 JavaScript 运行环境,对两种模块规范都提供了支持,但使用方式存在差异。
CommonJS:默认支持,无需额外配置
CommonJS 是 Node.js 的默认模块规范,Node.js 诞生之初便采用了 CommonJS 规范,所有 .js 后缀的文件(未特殊配置时)默认被解析为 CommonJS 模块,可直接使用 require() 和 module.exports,无需进行任何额外配置。
javascript
// utils.js:Node.js 中默认的 CommonJS 模块
module.exports = {
sayHello: function() {
return 'Hello Node.js CommonJS';
}
};
// main.js:直接运行 node main.js 即可
const { sayHello } = require('./utils.js');
console.log(sayHello()); // 输出:Hello Node.js CommonJS
ES6 模块:两种启用方式
Node.js 对 ES6 模块的支持是后续新增的,默认不启用,需要通过以下两种方式之一手动启用:
-
修改文件扩展名为
.mjs:将 ES6 模块文件的后缀改为.mjs,Node.js 会自动将其解析为 ES6 模块,可直接使用import和export。javascript// utils.mjs:ES6 模块 export function sayHello() { return 'Hello Node.js ES6 Module (.mjs)'; } // main.mjs:运行 node main.mjs 即可 import { sayHello } from './utils.mjs'; console.log(sayHello()); // 输出:Hello Node.js ES6 Module (.mjs) -
配置
package.json的type: "module":在项目根目录的package.json中添加"type": "module"配置,此时项目中所有.js后缀的文件都会被解析为 ES6 模块。json// package.json { "name": "module-demo", "version": "1.0.0", "type": "module" }javascript// utils.js:ES6 模块(因 package.json 配置,被解析为 ESM) export function sayHello() { return 'Hello Node.js ES6 Module (package.json)'; } // main.js:运行 node main.js 即可 import { sayHello } from './utils.js'; console.log(sayHello()); // 输出:Hello Node.js ES6 Module (package.json)
7. 动态导入
虽然 ES6 模块默认是静态加载,但二者都支持动态导入需求,只是实现方式不同。
CommonJS:天然支持动态导入
由于 CommonJS 是运行时加载,require() 函数可以接收动态拼接的路径参数,天然支持动态导入,无需额外语法支持。
javascript
// main.js:CommonJS 动态导入示例
function loadModule(moduleName) {
// 模板字符串拼接模块路径,支持动态导入
const module = require(`./${moduleName}.js`);
return module;
}
// 动态加载 utils 模块
const utils = loadModule('utils');
console.log(utils.sum(2, 3)); // 输出:5
ES6 模块:通过 import() 实现
ES6 模块提供了 import() 函数(属于动态导入语法,与静态 import 语句不同),该函数返回一个 Promise 对象,支持通过 .then() 回调或 await 语法处理异步加载结果,实现动态导入需求。
javascript
// utils.js:ES6 模块
export function sum(a, b) {
return a + b;
}
// main.js:ES6 模块动态导入示例
// 方式1:使用 .then() 回调
import('./utils.js').then(({ sum }) => {
console.log(sum(2, 3)); // 输出:5
});
// 方式2:使用 await(需在异步函数中)
async function loadUtils() {
const { sum } = await import('./utils.js');
console.log(sum(2, 3)); // 输出:5
}
loadUtils();
三、总结对比(详细对比表)
| 特性 | CommonJS | ES6 模块 |
|---|---|---|
| 加载时机 | 运行时动态加载 | 编译时静态加载 |
| 输出机制 | 值的拷贝(浅拷贝) | 值的只读引用(动态绑定) |
| 动态性 | 天然支持动态加载(条件、路径拼接) | 静态加载默认不支持,需通过 import() 实现动态导入 |
| this 指向 | 顶层 this 指向 module.exports | 顶层 this 指向 undefined |
| Tree Shaking | 不支持(运行时加载,无法静态分析) | 支持(编译时静态分析,可剔除未使用代码) |
| 使用环境 | 主要用于 Node.js 环境(默认支持) | 浏览器环境(原生支持)、Node.js 环境(需配置启用) |
| 顶层 await | 不支持(会阻塞模块加载,报错) | 支持(可直接在顶层使用,异步加载依赖) |
| 条件导入 | 支持(可嵌套在代码块中) | 不支持静态条件导入,仅 import() 可实现异步条件导入 |
四、结尾:适用场景与发展趋势
1. 适用场景
- CommonJS:更适合 Node.js 服务器端开发场景。服务器端模块加载无需考虑浏览器兼容性,且CommonJS 支持动态加载、循环依赖的部分加载机制,更贴合服务器端的动态需求(如根据运行环境加载不同配置),同时 Node.js 对 CommonJS 有完善的默认支持,迁移成本低。
- ES6 模块:更适合浏览器端开发场景,以及追求高性能、可优化的现代化项目。浏览器端原生支持 ES6 模块,无需额外打包转换(或少量转换),且支持 Tree Shaking 优化,可显著减小打包体积;同时 ES6 模块的静态加载、动态绑定特性,更利于代码的静态分析、类型检查(如 TypeScript 集成),是现代化前端工程(Vue、React 项目)的首选模块规范。
2. 发展趋势
从 JavaScript 生态的发展来看,ES6 模块是标准化、通用性更强的模块规范。它是 ECMAScript 官方制定的标准,具备跨环境(浏览器、Node.js)的通用性,而 CommonJS 是社区制定的非标准规范,仅在 Node.js 环境中占据主导地位。
目前,Node.js 对 ES6 模块的支持越来越完善,现代化前端工程也已全面拥抱 ES6 模块,打包工具(Webpack、Vite)、构建工具、框架都对 ES6 模块提供了深度优化。未来,ES6 模块将逐步成为 JavaScript 生态的主流模块规范,实现「一套模块语法,跨环境运行」的目标,而 CommonJS 则会在 Node.js 旧项目、遗留系统中继续发挥作用,作为 ES6 模块的补充存在。