0%

ES6模块与CommonJS模块的差异

随着前端应用日益复杂,javascript 模块化成为组织代码的必备手段。目前最主流的两种模块规范是 CommonJS 和 ES6 模块(ES Module)。CommonJS 主要用于 Node.js 服务端,而 ES6 模块则是 ECMAScript 官方标准,支持浏览器和现代 Node.js。虽然二者目标相似,但在语法、加载机制、导出值等方面存在显著差异。

语法差异

CommonJS 语法

  1. 导出:使用 module.exports 或 exports 对象。
  2. 导入:使用 require() 函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
// CommonJS 导出 (a.js)
const name = 'CommonJS';
const greet = () => console.log('Hello');
module.exports = { name, greet };

// 或者
exports.name = 'CommonJS';
exports.greet = () => console.log('Hello');

// CommonJS 导入 (b.js)
const mod = require('./a.js');
console.log(mod.name); // 'CommonJS'
mod.greet(); // 'Hello'

ES6 模块语法

  1. 导出:使用 export 关键字(命名导出或默认导出)。
  2. 导入:使用 import 关键字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ES6 导出 (a.js)
export const name = 'ES6 Module';
export function greet() {
console.log('Hello');
}
// 默认导出
export default function() {
console.log('Default export');
}

// ES6 导入 (b.js)
import defaultFn, { name, greet } from './a.js';
console.log(name); // 'ES6 Module'
greet(); // 'Hello'
defaultFn(); // 'Default export'

加载机制:静态 vs 动态

CommonJS 动态加载

  1. require 可以在代码任意位置调用,可以动态拼接路径、条件加载。
  2. 模块在运行时同步加载。
1
2
3
// 动态加载示例
const moduleName = './module-' + Math.random() + '.js';
const mod = require(moduleName); // 运行时确定路径

ES6 模块静态加载

  1. import 和 export 必须位于模块顶层,不能嵌套在条件块内。
  2. 模块依赖在编译时(静态分析)确定,有利于 tree shaking 和优化。
1
2
3
4
5
6
7
// 错误:import 不能在代码块中
if (condition) {
import mod from './mod.js'; // 语法错误
}

// 正确:静态导入
import mod from './mod.js';

虽然 ES6 也提供了动态导入 import() 函数(返回 Promise),但那是运行时的异步加载,属于例外。

导出值的绑定:值的拷贝 vs 值的引用

CommonJS:导出值的拷贝

CommonJS 模块导出的是值的浅拷贝,一旦模块执行完毕,导出值就固定了;模块内部的后续变化不会影响已导入的值(除非导出的是引用类型,修改引用内部属性会体现)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// counter.js
let count = 0;
module.exports = {
count,
increment() {
count++;
console.log('内部 count:', count);
}
};

// main.js
const { count, increment } = require('./counter.js');
console.log(count); // 0
increment(); // 内部 count: 1
console.log(count); // 0 (count 仍为 0,并未改变)

ES6 模块:动态只读绑定

ES6 模块导出的是值的动态只读引用,导入的变量与原始变量绑定,原始值变化时,导入值也会相应变化(但导入方不能修改导入的变量,除非它本身是对象)。

1
2
3
4
5
6
7
8
9
10
11
12
// counter.js
export let count = 0;
export function increment() {
count++;
console.log('内部 count:', count);
}

// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment(); // 内部 count: 1
console.log(count); // 1 (count 随内部变化)

this 指向

  1. CommonJS 中,模块顶层的 this 指向当前模块的 exports 对象。
  2. ES6 模块 中,模块顶层的 this 是 undefined。
1
2
3
4
5
// CommonJS 模块
console.log(this === module.exports); // true

// ES6 模块
console.log(this); // undefined

循环依赖处理

CommonJS 的循环依赖

CommonJS 遇到循环依赖时,会返回当前已执行的导出部分(未完成的导出可能是不完整的对象),容易导致问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// a.js
exports.done = false;
const b = require('./b.js');
console.log('a 中 b.done =', b.done);
exports.done = true;
console.log('a 执行完毕');

// b.js
exports.done = false;
const a = require('./a.js');
console.log('b 中 a.done =', a.done);
exports.done = true;
console.log('b 执行完毕');

// main.js
const a = require('./a.js');
// 输出顺序:
// b 中 a.done = false
// b 执行完毕
// a 中 b.done = true
// a 执行完毕

ES6 模块的循环依赖

由于 ES6 模块是动态绑定,导入的是值的引用,即使在循环依赖中也能获取到正确的值(但需要依赖顺序,且变量提升机制使函数声明等可提前访问)。

1
2
3
4
5
6
7
8
9
10
11
12
13
// a.mjs
import { b } from './b.mjs';
export const a = 'a';
console.log('a 模块执行');

// b.mjs
import { a } from './a.mjs';
export const b = 'b';
console.log('b 模块执行, a =', a);

// 运行 a.mjs,输出:
// b 模块执行, a = undefined (因为 a 尚未初始化)
// a 模块执行

ES6 模块的循环依赖通常比 CommonJS 更可预测,但若导入未初始化的变量会得到 undefined,需注意编码顺序。

同步 vs 异步

  1. CommonJS 模块加载是同步的,因为 Node.js 早期服务端读取本地文件很快,且无需考虑网络。
  2. ES6 模块 设计为异步加载,适合浏览器环境,可以与 <script type="module"> 配合,支持按需加载。静态 import 是异步的,但语法上看起来是静态声明;动态 import() 返回 Promise。

静态分析与优化

  1. ES6 模块 支持静态分析,可以在编译阶段确定导入导出关系,从而实现 tree shaking(摇树优化)、死代码消除等。
  2. CommonJS 的模块结构是动态的,难以静态分析,通常打包工具需要额外处理才能实现优化。

使用环境

  1. CommonJS 主要运行在 Node.js 环境(通过 require),浏览器中需要打包工具(如 Browserify、Webpack)才能使用。
  2. ES6 模块 现代浏览器原生支持 <script type="module">,Node.js 从 12.x 开始逐步稳定支持(需将文件后缀改为 .mjs 或 package.json 中设置 “type”: “module”)。目前二者在 Node.js 中可以共存,但互操作有一些限制。

总结

特性 CommonJS ES6 模块
语法 require / module.exports import / export
加载时机 运行时同步 编译时静态(静态导入)+ 运行时异步(动态导入)
导出绑定 值的拷贝(基本类型) 值的动态只读引用
顶层this exports 对象 undefined
循环依赖 返回已执行部分的拷贝 动态绑定,但可能遇到暂时性死区
静态分析 不支持 支持,可实现 tree shaking
适用环境 Node.js(原生)、浏览器(打包后) 现代浏览器、Node.js(需配置)