CommonJS模块采用同步静态解析路径,不支持运行时动态拼接;其核心是缓存机制——首次require执行并缓存module.exports,后续直接返回缓存对象,键为绝对路径;模块被函数包装实现作用域隔离,且天然支持单例与循环依赖处理。

CommonJS 模块本身不支持“动态加载”意义上的运行时路径拼接(比如 require('mod-' + version)),它本质上是同步、静态解析路径的模块系统。所谓“动态”,仅体现在 require 调用可写在条件分支或函数体内,但路径必须是确定的字符串字面量或能被静态分析出的常量表达式。真正关键且实用的是它的缓存机制——这是理解 CommonJS 行为的核心。
require 调用不是每次都读文件
Node.js 第一次执行 require('./utils') 时,会:读取文件内容 → 包装成函数 → 执行并生成 module.exports → 把结果存进 require.cache 对象。后续再调用相同路径的 require,直接返回缓存里的 exports 对象,跳过读取和执行。
- 缓存键是模块的绝对路径(如
/project/src/utils.js),不是相对路径或别名 - 即使多次
require('./utils')或require('../src/utils')(指向同一文件),只要解析后的绝对路径一致,就命中同一缓存项 - 修改文件后重启进程才能生效;热更新需手动
delete require.cache[filename]
模块作用域隔离靠函数包装
每个模块代码实际被包裹在一个立即执行函数中:
`(function (exports, require, module, __filename, __dirname) { /* 你的模块代码 */ });`这带来两个效果:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
立即学习“Java免费学习笔记(深入)”;
-
变量不会泄漏到全局:模块内
const helper = () => {}只在该函数作用域内有效 -
每次 require 都获得独立的 exports 引用:但注意——如果导出的是对象或函数,多个模块拿到的是同一个引用;若导出的是基本类型(如
module.exports = 42),则每次 require 返回的是副本值
缓存让单例模式天然成立
因为模块只执行一次,其内部状态(如私有变量、计数器、连接实例)在整个进程生命周期内保持唯一。例如:
`// db.jslet connection = null;
module.exports = () => {
if (!connection) connection = createConnection();
return connection;
};`
无论多少个文件 require('./db'),拿到的都是同一个连接实例。这不是设计出来的“特性”,而是缓存+单次执行的自然结果。
循环依赖时缓存起关键作用
当 a.js require b.js,而 b.js 又 require a.js 时,Node.js 不会卡死。它在第一次进入 a.js 执行前就已在缓存中创建空的 module.exports 对象;当 b.js 回头 require a.js,就拿到这个尚未执行完、但已存在的 exports 对象(可能为空或部分初始化)。这要求开发者避免在模块顶层依赖对方的未定义属性。

















