CommonJS 通过运行时同步加载、模块包装函数、缓存机制和部分导出处理循环依赖实现模块化。Node.js 将模块包裹在私有函数中执行,提供隔离作用域;require 同步阻塞读取、编译、执行并返回 exports;模块首次加载后缓存于 require.cache;循环依赖时返回已初始化的 module.exports 部分对象。

CommonJS 的运行时加载和同步导入,本质是 Node.js 在执行阶段直接读取文件、包装执行、缓存结果的一整套流程,不依赖构建工具或预编译。
模块被自动包装成函数执行
Node.js 不会直接运行你写的 math.js,而是在加载时把它裹进一个私有函数里:
(function(exports, require, module, __filename, __dirname) {
// 你的原始代码在这里
const add = (a, b) => a + b;
module.exports = { add };
});
这样就天然隔离了作用域,变量不会泄露到全局,exports 和 module 也成了每个模块可用的本地变量。
立即学习“Java免费学习笔记(深入)”;
require 是同步阻塞调用,立即返回导出对象
每次写 const utils = require('./utils'),Node.js 就立刻做三件事:
- 按路径查找文件(支持 .js/.json/.node 扩展名自动补全)
- 读取文件内容,编译并执行上面那个包装函数
- 把
module.exports的值返回给当前语句
整个过程在单线程中完成,不等异步操作,所以叫“同步导入”。这也意味着:如果 require 出现在 if 里,它只在条件为真时才执行——这是 CommonJS 支持动态加载的关键。
模块缓存让重复引入不重复执行
Node.js 内部维护一个 require.cache 对象,键是绝对路径,值是已执行的 Module 实例。第一次 require('./config') 会读文件、执行、存入缓存;之后再 require('./config'),直接从缓存取 module.exports,跳过所有 I/O 和执行步骤。
你可以随时查看:console.log(require.cache),里面就是所有已加载模块的快照。
循环依赖靠“部分导出”兜底
当 A require B,B 又 require A 时,Node.js 不会死锁。它会在 A 第一次执行到 require('./b') 时,把此时 A 已赋值的 module.exports(可能是空对象或部分属性)先交给 B 使用;等 A 执行完,再更新缓存。所以 B 拿到的 A 是“半成品”,但能避免崩溃。
这种行为不是错误,而是 CommonJS 明确设计的容错机制。


















