CJS 和 ESM 关键字/全局变量对比
📦 CommonJS (CJS)
CJS 是 Node.js 默认的老模块系统,使用 require 和 module.exports。
核心关键字 / 全局变量
require用于导入模块,例如:const fs = require("fs")module.exports导出一个模块的接口对象。exports是module.exports的引用,常用于简写导出。__filename当前模块的绝对路径(包含文件名)。__dirname当前模块所在目录的绝对路径。arguments在函数中依然可用,但不是模块特有的(ESM 中的顶层不再支持)。
特点
- 模块加载是 同步的(
require立即执行)。 - 每个文件就是一个模块,作用域隔离。
- Node.js 原生支持。
🌐 ECMAScript Modules (ESM)
ESM 是 JavaScript 官方标准,现代浏览器和 Node.js 都支持(需要 .mjs 或 package.json 配置 "type": "module")。
核心关键字 / 特性
import用于引入模块:import fs from "fs"import { readFile } from "fs"
export导出模块:export default function() {}export const x = 1
import.meta提供关于当前模块的元信息(例如:import.meta.url获取模块的文件路径 URL)。top-level await在 ESM 顶层作用域中可以直接使用await。
特点
- 模块加载是 异步的。
- 静态结构,编译时可确定依赖关系。
- 更加标准化,支持 Tree-shaking(构建时优化)。
🚧 差异对照表
| 特性 | CommonJS (CJS) | ECMAScript Modules (ESM) |
|---|---|---|
| 导入 | require() | import |
| 导出 | module.exports / exports | export / export default |
| 文件扩展名 | .cjs / .js | .mjs / .js |
| 文件路径变量 | __filename, __dirname | ❌(可用 import.meta.url 替代) |
| 模块元信息 | ❌ | import.meta |
异步顶层 await | ❌ | ✅ |
| 加载方式 | 同步 | 异步 |
👉 例如,ESM 中没有 __filename 和 __dirname,你需要用以下方式替代:
js
import { dirname } from 'node:path'
// ESM 中获取 __dirname 效果
import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)📦 CommonJS (CJS) — 清除模块缓存
CJS 使用 require() 加载模块时,会把模块对象缓存在 require.cache 里。如果想清除缓存,可以操作这个对象。
关键方式
js
// 加载模块
const mod = require('./foo.js')
// 清除缓存
delete require.cache[require.resolve('./foo.js')]
// 重新加载
const freshMod = require('./foo.js')require.resolve()会返回模块的绝对路径,用它作为 key 来删除缓存。- 删除后,再次
require()会重新执行模块代码。
⚠️ 注意:
- 如果模块被多个文件引用,删除缓存只影响 当前模块,依赖树中的引用关系可能导致“旧对象”仍在内存中。
- 所以要真正“重载”模块,有时需要递归删除依赖树的缓存。
🌐 ECMAScript Modules (ESM) — 清除模块缓存
ESM 的模块缓存存放在 import 的内部 Module Map 中,规范里没有暴露直接操作缓存的方法。 不过在 Node.js 环境 中,可以通过 动态导入 (import()) + 带查询参数 或 利用 vm / Loader Hooks 来实现类似效果。
常用方式 1:加查询参数绕缓存
js
const mod1 = await import(`./foo.js?update=${Date.now()}`)
const mod2 = await import(`./foo.js?update=${Date.now()}`)- 每次加上不同的 querystring,就会绕过缓存,强制重新加载。
常用方式 2:使用 import() + Module.createRequire
在 Node.js 18+ 里,可以通过 vm 的上下文重新加载模块,不过相对复杂。一般开发调试时用 动态 import() 加 query 就够了。
🚧 对比总结
| 功能 | CommonJS (CJS) | ECMAScript Modules (ESM) |
|---|---|---|
| 缓存位置 | require.cache | 内部 Module Map(不可直接访问) |
| 清除缓存方法 | delete require.cache[require.resolve()] | ❌ 无官方 API |
| 实际可行的清除方法 | 直接删除缓存并重新 require() | 动态 import() 加唯一参数绕过缓存 |
| 典型应用场景 | 热重载、插件系统 | 热重载、调试时强制刷新模块 |
