node.js中module.exports vs exports

如果你有过 Node.js 的开发经验,你可能熟悉用于将代码从一个模块导出到另一个模块的 module.exportsexports关键字。虽然它们乍一看似乎可以互换使用,但有充分的理由选择其中之一。

导入导出模式

让我们先来看一下 Node.js 中几种常见的 CommonJS 导入导出模式。

修改 module.exports 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// math.js
const add = (a, b) => {
return a + b;
};

const subtract = (a, b) => {
return a - b;
};

module.exports.add = add;
module.exports.subtract = subtract;

//index.js
const math = require("./math");

console.log(math.add(2, 3)); // 5
console.log(math.subtract(2, 3)); // -1

在第一种模式中,函数附加到 module.exports 对象的属性上。我们可以在 index.js 中看到正确的值被记录。

修改 exports 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// math.js
const add = (a, b) => {
return a + b;
};

const subtract = (a, b) => {
return a - b;
};

exports.add = add;
exports.subtract = subtract;

//index.js
const math = require("./math");

console.log(math.add(2, 3)); // 5
console.log(math.subtract(2, 3)); // -1

在第二种模式中,你将函数附加到exports对象的属性上。在 index.js 中仍然记录了正确的值。

现在,这引出了一个问题:当exports看起来可以用更少的按键来实现相同的结果时,为什么要使用 module.exports 呢?为了回答这个问题,让我们看一下以下两种模式。

module.exports 分配新对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// math.js
const add = (a, b) => {
return a + b;
};

const subtract = (a, b) => {
return a - b;
};

module.exports = {
add,
subtract,
};

//index.js
const math = require("./math");

console.log(math.add(2, 3)); // 5
console.log(math.subtract(2, 3)); // -1

在第三种模式中,将一个新对象分配给 module.exports。然后在index.js中记录了正确的值。

给 exports 分配新对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// math.js
const add = (a, b) => {
return a + b;
};

const subtract = (a, b) => {
return a - b;
};

exports = {
add,
subtract,
};

//index.js
const math = require("./math");

console.log(math.add(2, 3)); // TypeError: math.add is not a function
console.log(math.subtract(2, 3)); // TypeError: math.subtract is not a function
console.log(math); // {}

在第四种模式中,将一个新对象分配给 exports。然而,这种模式似乎不起作用,因为 math 看起来是一个空对象。让我们了解一下为什么。

JavaScript 中的对象引用

让我们重新思考一下 JavaScript 中的对象引用是如何工作的。当你将一个对象分配给另一个对象时,两个对象都指向同一个内存地址。修改一个对象也会修改另一个对象。让我们看看这个例子。

1
2
3
4
5
6
7
8
9
const superhero1 = {
name: "Bruce Wayne",
};

const superhero2 = superhero1; // 创建对 superhero1 的引用

superhero2.name = "Clark Kent"; // 修改 superhero2 的名字

console.log(superhero1); // { name: 'Clark Kent' } superhero1 的名字被更新了

在这个例子中,superhero1 superhero2都引用同一个superhero。修改 superhero2 也会修改 superhero1

然而,分配一个新对象会破坏引用。

1
2
3
4
5
6
7
8
9
10
11
const superhero1 = {
name: "Bruce Wayne",
};

let superhero2 = superhero1; // 创建对 superhero1 的引用

superhero2 = { name: "Clark Kent" }; // 分配会破坏引用

superhero2.name = "Barry Allen"; // 只有 superhero2 被修改

console.log(superhero1); // { name: 'Bruce Wayne' } superhero1 没有受到影响

在这种情况下,分配会破坏引用,修改 superhero2 不再影响 superHero1

module.exports vs. exports

现在我们了解了 JavaScript 中对象的工作原理,让我们将其与 module.exports exports 相关联起来。在Node.js中,module 是一个普通的 JavaScript 对象,具有一个exports属性。exports 是一个普通的JavaScript变量,恰好被设置为 module.exports。当你在另一个文件中 require 一个模块时,该模块内的代码被执行,并且只返回 module.exports

1
2
3
4
5
6
7
8
9
10
11
12
var module = { exports: {} };

var exports = module.exports;

// 通过对象引用将 exports 分配给 module.exports...

exports.add = add; // ...导致 module.exports.add = add
exports.subtract = subtract; // ...导致 module.exports.subtract = subtract

// module.exports 对象包含 add 和 subtract 属性

return module.exports;

然而,如果你将一个新对象分配给 exports,引用将被破坏,更新exports将不再更新 module.exports

1
2
3
4
5
6
7
8
9
10
11
12
var module = { exports: {} };

var exports = module.exports;

// 以下分配会破坏对象引用

exports = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
};

return module.exports; // module.exports = {}

如果你尝试访问导出对象上的 addsubtract,将会抛出错误,因为 module.exports 是空的。因此,虽然在第一个导入导出模式中 module.exports exports 看起来可以互换使用,但它们并不相同。

结论

什么时候应该选择 exports 而不是 module.exports?简短的答案是你可能不应该这样做。虽然 exports 可能更短,看起来更方便,但它可能会引起混淆,这是不值得的。请记住,exports 只是对 module.exports 的引用,将一个新对象分配给 exports 会破坏该引用。