跳到主要内容

exports.xxx = xxxmodule.exports = {} 有什么区别?

面试速答(30 秒版 TL;DR)

  • 真正被 require() 返回的,是 module.exports,不是 exports
  • exports 一开始只是一个方便书写的别名,它初始时指向 module.exports
  • 所以 exports.xxx = xxx 本质是在给 module.exports 挂属性,通常是有效的。
  • 但如果你写 module.exports = {},你是把导出对象整体换掉了;此时原来的 exports 还指着旧对象,两者不再同步。
  • 面试一句话:可以给 exports 挂属性,但不要用 exports = {} 重新赋值;要整体导出,请直接改 module.exports

心智模型:一个是真出口,一个是快捷引用

最稳的理解方式是:

function wrapper(exports, require, module) {
// 模块代码
}

进入模块时,大致相当于:

module.exports = {};
const exports = module.exports;

所以一开始:

  • exportsmodule.exports 指向同一个对象;
  • 你给任意一边“加属性”,通常都会体现在同一个对象上。

但注意,“指向同一个对象”不等于“永远绑定在一起”


一、为什么 exports.xxx = xxx 通常能工作

例如:

exports.add = (a, b) => a + b;
exports.sub = (a, b) => a - b;

本质上等价于:

module.exports.add = (a, b) => a + b;
module.exports.sub = (a, b) => a - b;

因为此时两者仍然引用同一个对象。

最终 require() 拿到的会是:

{
add: [Function],
sub: [Function]
}

所以面试里可以说:

  • exports 挂属性,本质是在修改 module.exports 当前指向的那个对象。

二、为什么 module.exports = {} 是另一回事

看这个例子:

module.exports = {
add(a, b) {
return a + b;
},
};

这不是“给原对象加属性”,而是:

  • 直接把 module.exports 指向了一个全新的对象。

这会导致:

  • require() 最终返回这个新对象;
  • 但原来的 exports 变量,仍然还指向旧对象。

所以从这一步开始:

  • exports !== module.exports

三、为什么 exports = {} 往往是错的

这是最容易答错的地方。

exports = {
add(a, b) {
return a + b;
},
};

很多人以为这和 module.exports = {} 一样,实际上不是。

因为这句做的事情只是:

  • 把当前模块作用域里的局部变量 exports 重新指向了新对象。

但真正要返回给外部的 module.exports 根本没变。

所以:

// bad.js
exports = {
value: 123,
};

// app.js
const result = require("./bad");
console.log(result); // {}

为什么是空对象?

  • 因为 require() 返回的是 module.exports
  • 而你改的是局部变量 exports 的指向,不是 module.exports

四、用一张图看清关系变化

这张图要表达的只有一句话:

  • 改属性没问题,改引用要分清你改的是谁。

五、什么时候该用 exports,什么时候该用 module.exports

适合用 exports.xxx = xxx

当你想导出多个命名成员时,这种写法很顺手:

exports.add = add;
exports.sub = sub;
exports.mul = mul;

适合用 module.exports = ...

当你想整体导出一个对象、函数、类、构造器时,更应该直接写:

module.exports = function createServer() {};

或者:

module.exports = class Queue {};

因为这时你导出的不是“给现有对象补几个字段”,而是“直接指定整个模块的导出值”。


六、最小对比例子

例 1:正确,给 exports 挂属性

exports.name = "node";
exports.version = 20;

结果:

require("./a"); // { name: 'node', version: 20 }

例 2:正确,直接替换 module.exports

module.exports = function sum(a, b) {
return a + b;
};

结果:

const sum = require("./b");

例 3:错误,用 exports = {}

exports = {
name: "wrong",
};

结果通常不是你想要的,因为真正返回的还是原来的 module.exports


七、为什么很多资料会说“不要混用”

因为像下面这样很容易出错:

exports.a = 1;
module.exports = {
b: 2,
};

最终导出是什么?

{ b: 2 }

原因是:

  • 前面 exports.a = 1 改的是旧对象;
  • 后面 module.exports = { b: 2 } 直接把出口换成了新对象;
  • 所以 a 丢了。

更稳的工程习惯是:

  • 要么始终用 exports.xxx = xxx 做“追加导出”;
  • 要么明确用 module.exports = ... 做“整体导出”;
  • 不要在同一个模块里前后混着改,除非你非常清楚引用关系。

典型题 & 标准答法

Q1:exportsmodule.exports 有什么关系?

  • exportsmodule.exports 的初始别名。
  • 真正导出的永远是 module.exports

Q2:为什么 exports.xxx = xxx 可以,exports = {} 不行?

  • 前者是给共享对象加属性。
  • 后者只是改了局部变量 exports 的指向,没有改真正的出口 module.exports

Q3:什么时候必须用 module.exports

  • 当你想整体导出一个函数、类、对象、构造器时。
  • 因为这类场景本质是替换导出值,而不是给原对象补字段。

Q4:为什么说不要混用?

  • 因为一旦 module.exports 被重新赋值,之前 exports 上做的改动可能全部失效。

易错点 / 坑

  • 以为 exports 才是真正返回值。
  • exports = {}module.exports = {} 当成同一种操作。
  • 在同一个模块里先写 exports.a = 1,后写 module.exports = {},最后又奇怪为什么 a 没了。
  • 只背语法结论,不知道它和 Node 模块包装函数的关系。

速记要点(可背诵)

  • 真正导出的只有 module.exports
  • exports 只是初始别名。
  • exports.xxx = xxx 是“改同一个对象”。
  • module.exports = {} 是“换导出对象”。
  • exports = {} 只是改局部变量,通常无效。