判断模块类型需看文件扩展名和package.json的"type"字段:.cjs强制CommonJS,.mjs强制ESM,.js配合"type":"module"为ESM,否则为CommonJS。
CommonJS 和 ES 模块(ESM)不是同一套机制,不能混用 require() 和 import,Node.js 中默认启用 ESM 需靠文件扩展名或 package.json 显式声明。
看执行环境和文件标识:
.cjs 结尾 → 强制 CommonJS.mjs 结尾 → 强制 ESM.js,且所在项目 package.json 有 "type": "module" → 默认 ESM.js,且 package.json 无 "type" 字段或值为 "commonjs" → 默认 CommonJSrequire() 在 ESM 文件中直接报错:ReferenceError: require is not defined
require() 和 import 的行为差异CommonJS 是运行时同步加载,ESM 是编译时静态分析 —— 这导致两者根本不可互换:
require() 可以写在条件语句里、函数内,甚至拼接路径:require('./' + name + '.js')
import 必须是顶层语句,路径必须是字符串字面量,不能动态;否则报错:Cannot use import statement outside a module 或 Dynamic imports must be of string literal
require() 返回的是模块的 exports 对象,可随意赋值:module.exports = class A {} 或 exports.foo = 1
import 只能解构或命名导入,且绑定是“实时只读引用”(非拷贝),被导入模块内部改了值,导入方能感知(如导出一个对象,其属性被修改)Node.js 允许 ESM 用 import 加载 CommonJS 模块,但行为受限:
module.exports = xxx(单个值),ESM 中可用 import xxx from 'pkg'
exports.a = 1; exports.b = 2,ESM 中必须写成 import * as mod from 'pkg',再通过 mod.a 访问
import { a } from 'pkg' 直接解构 —— 因为 CommonJS 没有静态 export 声明,ESM 无法做静态分析require() 无法加载原生 ESM 文件(会报错 ERR_REQUIRE_ESM),除非用 await import() 动态导入常见报错背后的真实原因:
TypeError: __dirname is not defined in ES module scope
→ ESM 中没有 __dirname 和 __filename,得用:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const filename = fileURLToPath(import.meta.url);
const dirname = dirname(__filename);
另一个高频问题:
Must use import to load ES Module
→ 你用 require('./util.mjs') 了,但 .mjs 是强制 ESM,只能用 import 或 await import() 加载。
跨模块混合时,最麻烦的不是语法,而是循环依赖处理逻辑完全不同:CommonJS 返回当前已执行部分的 exports,ESM 则抛出 ReferenceError(未初始化完成前访问)。