人人都会AI编程

exports 与 module.exports 的区别与本质

更新时间:2026-07-11

在 Node.js 的 CommonJS 模块系统中,每个文件被编译执行时,Node 会为该模块隐式地注入几个关键变量:requiremoduleexportsdirnamefilename。其中 module 是一个对象,代表当前模块本身,它有一个属性叫 module.exports这才是 Node.js 用来导出内容的真正出口。而 exports 只是一个指向 module.exports 的引用,目的是方便开发者向导出对象上添加属性。

两者的关系可以用一句简单的代码概括:

// Node.js 在模块内部做了类似这样的初始化
var exports = module.exports; // exports 是 module.exports 的引用

这意味着,当你通过 exports.xxx = value 的方式挂载属性时,实际上是在修改 module.exports 所指的那个对象,所以可以正常导出:

exports.hello = function() {
  console.log('hello');
};
// 实际等价于
module.exports.hello = function() {
  console.log('hello');
};

为什么给 exports 重新赋值就会“失效”?

如果用一个新的对象给 exports 赋值,就切断了 exportsmodule.exports 之间的引用关系。此后,exports 指向了一个新对象,但 module.exports 依然是原来的那个(空对象)。require 返回的永远是 module.exports,而不是 exports,因此后续给 exports 添加的任何内容都不会被外部访问到:

// ❌ 错误示例
exports = {
  hello: function() {
    console.log('hello');
  }
};
// 此时 module.exports 仍然是空对象 {},require 的结果是 {}

上面的代码在外部 require 后得到的只是一个空对象,hello 方法丢失了。正确的做法是直接给 module.exports 赋值:

// ✅ 正确:直接改写整个导出对象
module.exports = {
  hello: function() {
    console.log('hello');
  }
};

常见用法对比与最佳实践

| 操作方式 | 实际影响 | 是否推荐 |
|----------|----------|----------|
| exports.key = value | 向 module.exports 对象添加属性 | ✅ 推荐 |
| module.exports.key = value | 同上,更明确 | ✅ 推荐 |
| module.exports = {} | 替换整个导出对象 | ✅ 推荐(当需要导出一个构造函数或类时) |
| exports = {} | 断开了引用,module.exports 未被修改 | ❌ 绝对禁止 |

从工程实践角度看,为了避免团队成员混淆,很多团队直接选择只使用 module.exports 或只使用 exports 挂载属性,而不混用。例如:

// 风格一:挂载属性
exports.findOne = function() { ... };
exports.update = function() { ... };

// 风格二:导出整个对象
module.exports = {
  findOne() { ... },
  update() { ... }
};

这两种写法都不会出错,重点在于始终明确“真正生效的是 module.exports”。

底层原理:从 require 看返回值

require 函数在执行完模块代码后,会返回该模块的 module.exports 对象。它的伪代码大致如下:

function require(id) {
  // 1. 解析路径,定位文件
  // 2. 判断缓存,如有则直接返回 module.exports
  // 3. 新建一个 Module 实例,其 module.exports 初始为 {}
  // 4. 编译并执行模块代码(将 module, exports 等注入)
  // 5. 缓存 module
  // 6. 返回 module.exports
}

可见,无论模块内部如何使用 exports,外部拿到的永远是这个对象——module.exports。深刻理解这一点,就会明白为什么修改 exports 的引用会导致“导出失败”。

总结一句话

exports 是一个便捷的“别名”,指向 module.exports 的初始对象。只有通过添加属性的方式使用它(exports.foo = bar)才是安全的,直接给 exports 赋一个新值就会失败。需要导出单个构造函数、类或完全替换原有导出时,必须直接操作 module.exports 牢记这一点,可以避免绝大多数由模块导出引发的诡异 bug。