随着前端应用日益复杂,javascript 模块化成为组织代码的必备手段。目前最主流的两种模块规范是 CommonJS 和 ES6 模块(ES Module)。CommonJS 主要用于 Node.js 服务端,而 ES6 模块则是 ECMAScript 官方标准,支持浏览器和现代 Node.js。虽然二者目标相似,但在语法、加载机制、导出值等方面存在显著差异。
语法差异
CommonJS 语法
- 导出:使用 module.exports 或 exports 对象。
- 导入:使用 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 模块语法
- 导出:使用 export 关键字(命名导出或默认导出)。
- 导入:使用 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 动态加载
- require 可以在代码任意位置调用,可以动态拼接路径、条件加载。
- 模块在运行时同步加载。
1 2 3
| // 动态加载示例 const moduleName = './module-' + Math.random() + '.js'; const mod = require(moduleName); // 运行时确定路径
|
ES6 模块静态加载
- import 和 export 必须位于模块顶层,不能嵌套在条件块内。
- 模块依赖在编译时(静态分析)确定,有利于 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 指向
- CommonJS 中,模块顶层的 this 指向当前模块的 exports 对象。
- 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 异步
- CommonJS 模块加载是同步的,因为 Node.js 早期服务端读取本地文件很快,且无需考虑网络。
- ES6 模块 设计为异步加载,适合浏览器环境,可以与
<script type="module"> 配合,支持按需加载。静态 import 是异步的,但语法上看起来是静态声明;动态 import() 返回 Promise。
静态分析与优化
- ES6 模块 支持静态分析,可以在编译阶段确定导入导出关系,从而实现 tree shaking(摇树优化)、死代码消除等。
- CommonJS 的模块结构是动态的,难以静态分析,通常打包工具需要额外处理才能实现优化。
使用环境
- CommonJS 主要运行在 Node.js 环境(通过 require),浏览器中需要打包工具(如 Browserify、Webpack)才能使用。
- 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(需配置) |