先记住这个答案
Node.js 先看扩展名:.mjs 一定按 ESM 加载,.cjs 一定按 CommonJS 加载,不受任何配置影响。只有 .js 需要额外判断:从文件所在目录往上找最近的 package.json,里面写了 "type": "module" 就按 ESM,写了 "type": "commonjs" 或者根本没写就按 CommonJS。type 字段就是给 .js 文件指定默认格式的开关,默认值是 CommonJS,这是为了不破坏大量已有的老包。
.mjs是 ESM,.cjs是 CommonJS.js还要看包的type设置- 不要仅凭扩展名猜
.js模块类型 type只管本包内的.js,不影响依赖包
同一个文件,换个配置就报错
在一个目录里放一个 c.js,内容是 CommonJS 写法。
type 以后Shell# c.js 内容:const path = require('path'); console.log(typeof path)
echo '{}' > package.json
node c.js
# object,没写 type,按 CommonJS 运行
echo '{"type":"module"}' > package.json
node c.js
# ReferenceError: require is not defined in ES module scope
mv c.js c.cjs && node c.cjs
# object,.cjs 不看 type第二次运行时文件内容没变,只是 package.json 多了 "type": "module",c.js 就被当成了 ESM,而 ESM 里没有 require、module、exports、__dirname 这些变量。改成 .cjs 扩展名后又能跑了,因为扩展名的优先级更高。
反过来也一样:没有 type 字段的项目里,.js 文件写 import 语句,传统上会报 Cannot use import statement outside a module。我在 Node 22 上实测,它会先检测到 ESM 语法,改按 ESM 重新解析,同时打印一条警告,建议你在 package.json 里加上 "type": "module"。这个检测只是兜底,有额外开销,正式项目还是应该写清楚。
两种格式怎么互相引用
新项目一般整体用 ESM,但依赖里还有大量 CommonJS 包,互相引用的规则要清楚。
- ESM 引 CommonJS,CommonJS 引 ESM
- ESM 导入 CommonJS 一直都可以,
module.exports会作为默认导出,Node.js 也会尽量静态分析出具名导出,但分析不出来的只能从默认导出上取。 CommonJS 引 ESM 过去只能用await import(),因为require是同步的,而 ESM 可以包含顶层await。较新的 Node.js 版本支持直接require一个没有顶层await的 ESM 文件,但老版本不行,要兼容老环境的库仍然只能提供两份产物。
容易答错的地方
- 以为
type看的是项目根目录的package.json - 看的是离文件最近的那一个。monorepo 里每个子包有自己的
package.json,各自的type各管各的。构建产物目录里放一个小的package.json覆盖格式,也是常见做法。 - 以为设置
type会影响node_modules里的依赖 - 依赖包有自己的
package.json,按它自己的type解析。你把项目改成"type": "module",不会让某个 CommonJS 依赖突然变成 ESM。 - 忘了配置文件也受影响
- 项目改成
"type": "module"后,根目录下用module.exports写的.eslintrc.js、postcss.config.js、jest.config.js会立刻报错。改名成.cjs,或者改用export default写法。
面试官还会怎么问?
TypeScript 里怎么对应?
TypeScript 有对应的 .mts 和 .cts 扩展名,编译后分别产出 .mjs 和 .cjs。.ts 文件编译成什么格式,在 module 设为 nodenext 时同样取决于最近的 package.json 的 type。
node -e 执行的代码按什么格式?
字符串输入和标准输入默认按 CommonJS。可以用 --input-type=module 指定按 ESM 执行。
为什么不直接把默认值改成 ESM?
npm 上有海量没写 type 字段的 CommonJS 包,默认值一改它们全部会坏。所以 Node.js 选择保持默认 CommonJS,让新项目显式声明 "type": "module"。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。