exports.xxx = xxx 和 module.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;
所以一开始:
exports和module.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:exports 和 module.exports 有什么关系?
exports是module.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 = {}只是改局部变量,通常无效。