ES6模块与CommonJS是两套不同系统:前者静态分析、实时绑定、仅顶层import;后者同步加载、运行时赋值、支持动态require。二者混用会导致undefined、副作用丢失、循环引用异常等运行时问题。

ES6 模块(import/export)和 CommonJS(require/module.exports)不是“两种写法选一个就行”,而是运行在不同环境、解析时机、导出机制都不同的两套系统。混用或误解差异,轻则报 ReferenceError: require is not defined,重则出现值为 undefined、副作用未执行、循环引用静默失败等问题。
CommonJS 是同步加载 + 运行时赋值
Node.js 默认使用 CommonJS(除非显式启用 "type": "module")。它的 require() 是同步读取文件、立即执行模块代码、把 module.exports 的**当前值**拷贝出去。
-
require()可以出现在任意位置(比如 if 语句里),支持动态路径:require('./' + name + '.js') -
module.exports = { a: 1 }和exports.a = 1等价;但一旦重新赋值exports = { a: 1 },就断开了与module.exports的引用,外部将收不到任何东西 - 循环引用时,返回的是被
require()时那个模块已执行完的部分(可能只是空对象),不会报错但结果不可靠
ES6 模块是静态分析 + 实时绑定
浏览器原生支持 import/export,打包工具(如 Webpack、Vite)也默认按 ES 模块语义处理。它在代码解析阶段就确定依赖关系,不执行模块体,所有导入都是对原始绑定的**实时引用**。
-
import必须在顶层作用域,不能写在 if 或函数里;路径必须是字符串字面量(不能拼接) -
export导出的是绑定,不是值拷贝:如果模块内let count = 0,又export { count },外部 import 后修改count,原始模块里的count也会变 - 循环引用时,ES 模块会建立“临时空壳”,等双方都声明完再链接绑定,行为可预测(但仍有陷阱,比如默认导出对象的属性未定义)
Node.js 中两者共存的真实限制
Node.js 从 v12 起支持 ES 模块,但和 CommonJS 并非无缝互通。关键限制不是语法,而是加载器隔离:
立即学习“Java免费学习笔记(深入)”;
- 一个
.mjs文件或"type": "module"的package.json下的.js文件,只能用import;里面写require()会直接报错 -
import不能直接加载 CommonJS 模块的module.exports对象 —— 它会被包装成default属性:import express from 'express'; // ✅ 实际拿到的是 { default: expressFn, __esModule: true }而import * as express from 'express'则得到命名空间对象,需通过express.default访问 - CommonJS 中
require('pkg')加载 ES 模块包时,只能拿到其default导出(如果有的话),无法访问具名导出
打包工具里为什么有时“看起来能混用”?
Webpack/Vite/Rollup 等工具做了大量兼容层工作,比如自动把 require() 转成 import、把 CommonJS 的 module.exports 映射为 export default。但这只是构建时的“模拟”,不代表运行时语义一致。
- 开发时用 Vite 启动,
import { foo } from './utils.cjs'能跑,是因为 Vite 把它当 ES 模块解析并重写了导出逻辑 - 但若该
.cjs文件里有require('fs'),生产环境直连 Node 执行就会报错 —— 因为fs在浏览器里根本不存在 - 最稳妥的做法:明确项目类型(
"type": "module"),统一用import/export;若必须用 CommonJS 第三方包,优先查它是否提供 ESM 版本(看package.json中的exports字段)
真正容易被忽略的点在于:模块系统差异最终会暴露在运行时行为上,而不是编译报错。比如一个工具函数导出后,在另一个模块里被修改却没生效,或者初始化逻辑在 import 阶段就被跳过 —— 这些都不是语法错误,而是绑定模型和执行时机的根本不同导致的。











