ES6模块与CommonJS是两套不同系统:前者静态分析、实时绑定、仅顶层import;后者同步加载、运行时赋值、支持动态require。二者混用会导致undefined、副作用丢失、循环引用异常等运行时问题。
ES6 模块(import/export)和 CommonJS(require/module.exports)不是“两种写法选一个就行”,而是运行在不同环境、解析时机、导出机制都不同的两套系统。混用或误解差异,轻则报 ReferenceError: require is not defined,重则出现值为 undefined、副作用未执行、循环引用静默失败等问题。
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() 时那个模块已执行完的部分(可能只是空对象),不会报错但结果不可靠浏览器原生支持 import/export,打包工具(如 Webpack、Vite)也默认按 ES 模块语义处理。它在代码解析阶段就确定依赖关系,不执行模块体,所有导入都是对原始绑定的**实时引用**。
import 必须在顶层作用域,不能写在 if 或函数里;路径必须是字符串字面量(不能拼接)export 导出的是绑定,不是值拷贝:如果模块内 let count = 0,又 export { count },外部 import 后修改 count,原始模块里的 count 也会变Node.js 从 v12 起支持 ES 模块,但和 CommonJS 并非无缝互通。关键限制不是语法,而是加载器隔离:
.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 访问require('pkg') 加载 ES 模块包时,只能拿到其 default 导出(如果有的话),无法访问具名导出Webpack/Vite/Rollup 等工具做了大量兼容层工作,比如自动把 require() 转成 import、把 CommonJS 的 module.exports 映射为 exp。但这只是构建时的“模拟”,不代表运行时语义一致。
ort default
import { foo } from './utils.cjs' 能跑,是因为 Vite 把它当 ES 模块解析并重写了导出逻辑.cjs 文件里有 require('fs'),生产环境直连 Node 执行就会报错 —— 因为 fs 在浏览器里根本不存在"type": "module"),统一用 import/export;若必须用 CommonJS 第三方包,优先查它是否提供 ESM 版本(看 package.json 中的 exports 字段)真正容易被忽略的点在于:模块系统差异最终会暴露在运行时行为上,而不是编译报错。比如一个工具函数导出后,在另一个模块里被修改却没生效,或者初始化逻辑在 import 阶段就被跳过 —— 这些都不是语法错误,而是绑定模型和执行时机的根本不同导致的。