一、每个 CJS 文件都被包装成函数

1
2
3
(function (exports, require, module, __filename, __dirname) {
// 写的代码
});

包装带来:

  • 作用域隔离:顶层变量不会污染全局。
  • 注入五个参数:exports、require、module、__filename、__dirname 是形参,不是全局变量,每个模块独立。
  • 导出通道:module.exports 作为参数传入,模块代码往它上面挂东西。

二、module、exports、module.exports 的关系

1
2
const module = { exports: {} };
const exports = module.exports; // 初始指向同一对象
  • 挂属性安全:exports.a = 1 等价于 module.exports.a = 1,因为操作同一对象。
  • 整体赋值危险:exports = { a: 1 } 断开引用,module.exports 还是空对象,导出失效。
  • 整体替换正确写法:module.exports = { a: 1 }。
  • Node 最终导出的是 module.exports 的值,不是 exports 变量。
  • module 对象本身不暴露给使用者,require 拿不到 module.id、module.filename 等。

三、require 的完整流程

1
2
3
4
5
6
7
8
9
10
11
1. 解析路径(补扩展名、找 index.js、查 node_modules)
2. 查缓存 Module._cache[filename]
├─ 命中 → 直接返回 .exports(不重新执行)
└─ 未命中 ↓
3. 创建 module 对象 { exports: {}, loaded: false, ... }
4. 读文件内容(fs.readFileSync)
5. 拼成包装函数(字符串 → vm.runInThisContext → 函数)
6. 执行包装函数,传入 exports/require/module/__filename/__dirname
(代码在此运行,可能改写 module.exports)
7. module.loaded = true,写入缓存
8. 返回 module.exports 的值

重点:

  • 缓存存的是整个 module 对象,命中时取 .exports。
  • 返回的是 module.exports 的值,不一定是对象(可能是函数、数字、字符串等)。
  • 同一模块多次 require,代码只执行一次,返回同一引用。
  • require 返回的不是 module 对象本身。

每个 CJS 文件被包成函数执行,module.exports 是唯一导出通道,exports 只是它的初始别名;require 查缓存、执行包装函数、返回 module.exports 的值。