CommonJS模块缓存机制确保同一绝对路径模块仅执行一次并复用module.exports,键为绝对路径,支持单例、影响循环引用处理,且通过包装函数隔离作用域。

CommonJS 的模块缓存机制是 Node.js 模块系统的核心设计之一,它决定了“同一个模块被多次 require 时,代码只执行一次,且始终返回同一份导出对象”。这不是可选优化,而是默认行为,直接影响程序状态、单例实现和循环引用处理。
模块缓存发生在 require 的第一环节
每次调用 require('./xxx') 时,Node.js 会先检查内部缓存对象 Module._cache。如果该模块的绝对路径已存在缓存中,就直接返回 module.exports,跳过后续所有步骤——包括文件读取、包装、编译、执行。
- 缓存键是模块的绝对路径(如 /Users/me/project/utils.js),不是相对路径或模块名
- 即使通过不同路径引入同一文件(比如
require('./utils')和require('../src/utils')),只要解析后绝对路径不同,就会被视为两个模块,各自加载、各自缓存 - 核心模块(如
fs、path)也走缓存流程,但它们在启动时已预加载并驻留内存
模块只执行一次,但 exports 可被多次访问
模块顶层代码(非函数定义部分)仅在首次加载时运行。后续 require 返回的是上次执行后 module.exports 的最终值——哪怕这个值是对象、类实例或带内部状态的函数。
- 例如:一个计数器模块
counter.js中有let count = 0; module.exports = { inc() { return ++count; } };无论你在 5 个地方 require 它,调用inc()都会共享同一个count - 这天然支持单例模式,无需手动实现,也不依赖全局变量
- 注意:若模块导出的是原始值(如
module.exports = 42),后续修改该变量不会影响已缓存的导出值,因为原始值是拷贝传递
require 包装函数保障作用域隔离
Node.js 在执行每个模块前,会把它的源码包裹进一个函数:
(function (exports, require, module, __filename, __dirname) { /* 你的代码 */ });
这个包装确保了:
- 模块内声明的变量、函数不会泄漏到全局作用域
- 每次 require 创建的新模块实例都有独立的
exports和module对象 - 但一旦模块执行完毕,它的
module.exports被缓存,后续 require 就不再新建实例,而是复用该缓存对象
缓存对循环引用的影响
当 A require B、B 又 require A 时,缓存机制防止了无限递归。Node.js 在进入 B 加载前,会先把 A 的空模块对象(module.exports = {})放入缓存;B 执行中 require A 时,拿到的就是这个尚未执行完的中间状态对象。
- 这意味着循环引用下,模块可能拿到的是“不完整”的导出对象,需避免在模块顶层依赖对方未定义的属性
- 但只要双方都使用
module.exports赋值(而非覆盖exports),最终导出内容仍能正确同步
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











