JavaScript模块化有ES6(静态、活绑定、编译时加载)和CommonJS(动态、值拷贝、运行时加载)两大体系,二者在加载时机、导出机制、循环依赖处理及环境支持上存在本质差异。

JavaScript模块化经历了从无到有、从手动管理到标准统一的过程。ES6模块(import/export)是语言原生支持的标准化方案,而CommonJS(require/module.exports)是Node.js早期采用的运行时模块系统,二者在设计目标、加载机制和使用场景上有本质差异。
模块加载时机:编译时 vs 运行时
ES6模块在代码解析阶段就确定依赖关系,即“静态分析”,所有import必须位于顶层作用域,不能放在条件语句或函数中。这使得工具可以做摇树优化(tree-shaking)、提前报错和跨文件类型推导。
CommonJS则是在代码执行时动态加载,require()可以出现在任意位置,甚至可拼接路径、配合变量使用,灵活性高但牺牲了静态可分析性。
-
import x from './a.js'—— 必须写死路径,不可动态 -
const x = require('./' + name)—— CommonJS允许这种动态引入
导出与导入机制:绑定 vs 值拷贝
ES6模块导出的是**活绑定(live binding)**:导出的变量与原始模块内部保持同步。如果模块A导出一个对象,模块B导入后修改该对象属性,模块A内部也能感知变化。
立即学习“Java免费学习笔记(深入)”;
CommonJS导出的是**值的浅拷贝**(更准确说是导出时module.exports指向的对象快照)。后续对原对象的重新赋值不会影响已导入的引用,但对象内部属性的修改仍可见(因为是同一内存地址)。
- ES6:
export let count = 0; setTimeout(() => count++, 100);导入方会看到更新后的值 - CommonJS:
module.exports = { count: 0 };后续改写module.exports = { count: 1 },老导入不受影响
循环依赖处理方式不同
ES6模块在遇到循环导入时,会返回一个**未初始化的空模块对象**,待模块执行完毕再填充导出内容。若访问尚未执行完的导出值,会报undefined或ReferenceError(取决于是否已声明)。
CommonJS在循环require时,直接返回当前模块已执行部分的exports对象(可能是不完整的),因此更易出现undefined属性,但通常不会报错。
- A.js → import B; B.js → import A;A中访问B的某个导出,可能得到
{}或undefined - CommonJS下,A中
require('./b')可能拿到B中已赋值的部分exports,比如{ init: fn },即使B还没执行完
环境支持与互操作现状
现代浏览器原生支持type="module"脚本,Node.js自v12起默认启用ESM(需.mjs扩展名或"type": "module"字段)。但CommonJS仍是Node生态大量包的基础格式。
两者不能直接混用:import不能直接导入CommonJS模块的require结果,反之亦然。Node提供createRequire和import.meta.resolve等API辅助桥接,打包工具(如Webpack、Vite)则通过自动包装实现兼容。
- ESM中想用CommonJS包:可直接
import pkg from 'pkg',Node会自动适配(前提是包有exports字段声明) - CommonJS中想用ESM:需用
await import()动态导入,不能用require()
不复杂但容易忽略。理解差异有助于避开构建报错、循环依赖陷阱和意外的值不更新问题。











