在Node.js中,module.exports与exports有何不同?

861

我在一个 Node.js 模块中找到了以下合同:

module.exports = exports = nano = function database_module(cfg) {...}

我想知道module.exportsexports之间的区别,以及为什么两者都在这里被使用。


6
为后人留存:http://nodejs.org/docs/latest/api/modules.html#module.exports - orftz
8
更新“为后人留存”链接:http://nodejs.org/docs/latest/api/modules.html#modules_module_exports。 - Zeke
10
这段话的核心是引用(references)的概念。可以将 exports 理解为一个指向 module.exports 的局部变量对象。如果你改变 exports 的值,就会失去对 module.exports 的引用,而 module.exports 是你所公开的公共接口。请注意,翻译时不要改变原意。 - Gabriel Llamas
18
简要概述:exportsmodule.exports指向同一个对象,除非您重新分配其中一个。最终会返回module.exports。因此,如果您将exports重新分配为函数,则不应期望返回该函数,因为它不会被返回。但是,如果您像这样赋值函数exports.func = function...,则结果将具有func属性和函数作为值。因为您将属性添加到exports所指向的对象中。 - Muhammad Umer
2
不理解为什么要同时提供 module.exportsexports。为什么不只提供 module.exports 以避免这种混淆呢? - Freewind
显示剩余8条评论
24个回答

653
尽管这个问题早就得到了回答和接受,但我只是想分享一下我的意见。
你可以想象一下,在你的文件的开头,可能会有类似以下的内容(仅供解释):
var module = new Module(...);
var exports = module.exports;

enter image description here

无论你做什么,只要记住当你从其他地方引用模块时,module.exports而不是exports将会被返回。所以当你做如下操作时:
exports.a = function() {
    console.log("a");
}
exports.b = function() {
    console.log("b");
}

你正在向module.exports指向的对象添加两个函数ab,因此返回结果的typeof将是一个object{ a: [Function], b: [Function] }
当然,在这个例子中,如果你使用module.exports而不是exports,你将得到相同的结果。
这是当你希望你的module.exports像一个导出值的容器一样运作的情况。然而,如果你只想导出一个构造函数,那么你应该了解一些关于使用module.exportsexports的事情。请记住,当你需要某个东西时,将返回module.exports,而不是exports
module.exports = function Something() {
    console.log('bla bla');
}

现在typeof返回的结果是'function',你可以使用require来引入它并立即调用它,例如:
var x = require('./file1.js')();,因为你将返回的结果覆盖为一个函数。
然而,使用exports时,你不能像这样使用:
exports = function Something() {
    console.log('bla bla');
}
var x = require('./file1.js')(); //Error: require is not a function

因为使用exports时,引用不再指向指向module.exports的对象,所以exportsmodule.exports之间不再有关联。在这种情况下,module.exports仍然指向空对象{},将被返回。
另一个主题中的接受答案也应该有所帮助: JavaScript是按引用传递的吗?

5
这里有解释:JavaScript是按引用传递吗?exports.a = function(){}; 可以运行,但是 exports = function(){}; 不能运行。 - cirpo
46
最终这个答案解释得很清楚了。基本上,"export"指的是一个对象,你可以向其中添加属性,但如果你将其重新赋值为函数,则不再将属性附加到原始对象上。现在"export"指向函数,而"module.exports"仍然指向那个对象,因为它是返回的内容。你可以说"export"已经被基本上垃圾回收了。 - Muhammad Umer
16
那么,使用 exports 的意义是什么?如果只是变量重新赋值,为什么不总是使用 module.exports?这对我来说似乎很困惑。 - user677526
2
@jedd.ahyoung 写exports.something比写module.exports.something更简便。 - Srle
6
通过向exports添加属性,您实际上确保返回了一个“典型”的模块导出对象。相比之下,通过使用module.exports,您可以返回任何想要的值(原始值、数组、函数),而不仅仅是对象(这是大多数人期望的格式)。因此,module.exports提供了更多的功能,但也可以用于导出非典型值(如原始值)。相比之下,exports更为限制,但更安全(只要简单地向其添加属性而不重新分配它)。 - Marcus Junius Brutus
显示剩余3条评论

462

设置module.exports允许在required时像调用函数一样调用database_module函数。仅设置exports将不允许导出函数,因为Node.js导出的是module.exports所引用的对象。以下代码将不允许用户调用该函数。

module.js

以下代码是无效的。

exports = nano = function database_module(cfg) {return;}

如果设置了module.exports,下面的代码将会起作用。

module.exports = exports = nano = function database_module(cfg) {return;}

控制台

var func = require('./module.js');
// the following line will **work** with module.exports
func();

基本上,node.js不会导出当前exports所引用的对象,而是导出了原始引用exports的属性。虽然Node.js会导出module.exports所引用的对象,使您可以像调用函数一样调用它。


第二个最不重要的原因

他们同时设置了module.exportsexports,以确保exports没有引用先前导出的对象。通过同时设置两者,您可以使用exports作为简写,并避免日后可能出现的潜在错误。

使用exports.prop = true而不是module.exports.prop = true可以节省字符并避免混淆。


8
@ajostergaard: 这只是OP的示例中使用的的名称。在该模块中,它允许作者编写类似于nano.version = '3.3'而不是module.exports.version ='3.3'之类的语句,这样读起来更清晰一些。(请注意,nano是一个局部变量,在设置模块导出之前稍微声明了一下。) - josh3736
3
@lime - 谢谢 - 我很高兴这在很大程度上是无关紧要的,因为如果不是这样,那就意味着我完全误解了一切。 :-| :) - ostergaard
嘿,Lime,这是一个相当老的回答,但我希望你能澄清一些事情。如果我设置了 module.exports没有设置 exports,我的代码还能正常工作吗?感谢任何帮助! - Asad Saeeduddin
1
@Asad 是的,只要你设置了 module.exports,这个函数就可以正确导出。 - Lime
我不知道为什么他们没有将“set”、“Proxy”应用于“exports”。 - Константин Ван

218

基本上,答案在于当一个模块被通过 require 语句引入时实际发生了什么。假设这是第一次需要该模块。

例如:

var x = require('file1.js');

file1.js的内容:

module.exports = '123';
当执行上述语句时,会创建一个Module对象。它的构造函数是:

当执行上述语句时,将创建一个Module对象。它的构造函数是:

function Module(id, parent) {
    this.id = id;
    this.exports = {};
    this.parent = parent;
    if (parent && parent.children) {
        parent.children.push(this);
    }

    this.filename = null;
    this.loaded = false;
    this.children = [];
}

正如你所看到的,每个模块对象都有一个名为 exports 的属性。这最终作为 require 的一部分返回。

require 的下一步是将 file1.js 的内容封装到一个匿名函数中,如下所示:

(function (exports, require, module, __filename, __dirname) { 
    //contents from file1.js
    module.exports = '123;
});

下面是对这个匿名函数的调用方式,这里的module指的是之前创建的Module对象。

(function (exports, require, module, __filename, __dirname) { 
    //contents from file1.js
    module.exports = '123;
}) (module.exports,require, module, "path_to_file1.js","directory of the file1.js");

正如我们在函数中所看到的,exports 形式参数是指 module.exports。本质上,这是为模块程序员提供的一种便利。

然而,这种便利需要谨慎使用。无论如何,如果尝试将一个新对象分配给 exports,请确保按照以下方式进行。

exports = module.exports = {};

如果我们按照错误的方法进行操作,module.exports仍将指向作为模块实例的一部分创建的对象。

exports = {};

因此,向上述导出对象添加任何内容都不会对module.exports对象产生影响,也不会作为require的一部分被导出或返回。


9
这里可能让您有些困惑:exports = module.exports = {};(意为:输出等于模块的输出,都为空对象)。 - Giant Elk
2
我认为这应该是最好的答案,它解释了为什么func()在@William的回答中失败了! - turtledove
3
在代码的最后一行添加“exports = module.exports = app;”我并没有看到任何优势。似乎“module.exports”会被导出,而我们永远不会使用“exports”,因为它又在代码的最后一行。那么,为什么不简单地添加“module.exports = app;”呢? - lvarayut

108

最初,module.exports=exports,而require函数返回的是module.exports所引用的对象。

如果我们向该对象添加属性,比如说exports.a=1,那么module.exports和exports依然指向同一个对象。因此,如果我们调用require并将模块分配给一个变量,那么这个变量就有一个名为a的属性,其值为1;

但是,如果我们覆盖它们中的一个,例如exports=function(){},那么它们现在是不同的:exports指向一个新对象,而module.exports指向原始对象。如果我们要求文件,它将不会返回新对象,因为module.exports没有引用到新对象。

对于我来说,我会继续添加新属性,或将它们都覆盖为一个新对象。只覆盖其中一个是不正确的。并且请记住,module.exports才是真正的老板。


4
是的,这实际上是正确的答案。它简洁明了。其他人可能是对的,但充满花哨的术语,并没有专注于这个问题的确切答案。 - Khoa

71

exportsmodule.exports 是相同的,除非您在模块内部重新分配 exports

最简单的思考方式是将此行隐式放置在每个模块的顶部。

var exports = module.exports = {};

如果在你的模块中重新赋值了exports,那么你就在模块内部对其进行了重新赋值,它将不再等于module.exports。这就是为什么如果你想要导出一个函数,你必须这样做:

module.exports = function() { ... }

如果你只是将你的function() { ... }分配给exports,那么你会重新分配exports,使其不再指向module.exports

如果你不想每次都用module.exports来引用你的函数,可以这样做:

module.exports = exports = function() { ... }

请注意,module.exports是最左边的参数。

附加属性到exports上不同于重新分配它。这就是为什么这个能够工作。

exports.foo = function() { ... }

37

JavaScript按引用传递对象

这与JavaScript中通过引用传递对象的方式有微妙的不同。

exportsmodule.exports都指向同一个对象。 exports是一个变量,而module.exports是模块对象的属性。

假设我写了以下代码:

exports = {a:1};
module.exports = {b:12};

exportsmodule.exports 现在指向不同的对象。修改 exports 不再修改 module.exports

当 import 函数检查 module.exports 时,它会得到 {b:12}


1
JavaScript 不是按引用传递。 - xehpuk
我读到的最佳答案。但是有一个问题:如果 const b = require() 让我得到 {b:12},我如何导入以获得 {a:1} - GoodMan

14

我只是做了一些测试,结果发现,在Node.js的模块代码中,应该像这样:

var module.exports = {};
var exports = module.exports;

抱歉,我只能按照英文进行回答。
exports = function(){}; // this will not work! as it make the exports to some other pointer
module.exports = function(){}; // it works! cause finally nodejs make the module.exports to export.

2:

exports.abc = function(){}; // works!
exports.efg = function(){}; // works!

3: 但是,在这种情况下

module.exports = function(){}; // from now on we have to using module.exports to attach more stuff to exports.
module.exports.a = 'value a'; // works
exports.b = 'value b'; // the b will nerver be seen cause of the first line of code we have do it before (or later)

Lyman,所以module.exports是Node使用的“真正的东西”,但在某个时候,您需要将所有的exports添加到module.exports中,除非您正在使用exports.namespace(上面的第2种情况),在这种情况下,似乎就像Node运行了一个extends(module.exports, exports);exports的所有“命名空间”添加到module.exports对象中?换句话说,如果您正在使用exports,那么您可能希望在其上设置属性? - Cody

13
了解区别,您首先需要了解Node.js在运行时对每个模块所做的操作。Node.js为每个模块创建一个包装函数:

要了解区别,必须先了解Node.js在运行时对每个模块所做的操作。Node.js为每个模块创建一个包装函数:

 (function(exports, require, module, __filename, __dirname) {

 })()

请注意第一个参数exports是一个空对象,第三个参数module是一个具有多个属性的对象,其中一个属性名为exports。这就是exports的来源和module.exports的来源。前者是一个变量对象,后者是module对象的属性。

在模块内部,Node.js会自动执行以下操作:module.exports = exports,最终返回module.exports

因此,如果你重新分配某个值给exports,它对module.exports没有任何影响。(仅仅因为exports指向另一个新对象,而module.exports仍然保持旧的exports

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

exports = { a: 1 }
console.log(module.exports) // {}

但是如果您更新exports的属性,它肯定会影响到module.exports。因为它们都指向同一个对象。

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

exports.a = 1;
module.exports.b = 2;
console.log(module.exports) // { a: 1, b: 2 }

还要注意,如果您将另一个值重新分配给module.exports,那么对于exports的更新似乎就毫无意义了。 因为module.exports指向另一个对象,所以对exports的每个更新都将被忽略。

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

exports.a = 1;
module.exports = {
  hello: () => console.log('hello')
}
console.log(module.exports) // { hello: () => console.log('hello')}

13

以下是摘自Manning出版社的《node.js in action》中关于Node.js模块的良好描述。
最终在应用程序中导出的内容是module.exports。而exports只不过是对module.exports的全局引用,最初被定义为空对象,可以向其中添加属性。因此,exports.myFunc仅仅是module.exports.myFunc的简写形式。

因此,如果将exports设置为其他任何值,都会破坏module.exportsexports之间的引用关系。因为真正被导出的是module.exports,所以exports将无法按预期工作-它不再引用module.exports。如果要保持这个链接,可以按以下步骤让 module.exports再次引用 exports:

module.exports = exports = db;

9
我经历了一些测试,我认为这可能会阐明这个问题... app.js:
var ...
  , routes = require('./routes')
  ...;
...
console.log('@routes', routes);
...

“/routes/index.js”的版本:”
exports = function fn(){}; // outputs "@routes {}"

exports.fn = function fn(){};  // outputs "@routes { fn: [Function: fn] }"

module.exports = function fn(){};  // outputs "@routes function fn(){}"

module.exports.fn = function fn(){};  // outputs "@routes { fn: [Function: fn] }"

我甚至添加了新文件:

./routes/index.js

module.exports = require('./not-index.js');
module.exports = require('./user.js');

./routes/not-index.js:
exports = function fn(){};

"./routes/user.js":
exports = function user(){};

我们得到的输出为"@routes {}"。

./routes/index.js

module.exports.fn = require('./not-index.js');
module.exports.user = require('./user.js');

"./routes/not-index.js":
exports = function fn(){};

./routes/user.js:
exports = function user(){};

我们得到的输出是 "@routes { fn: {}, user: {} }"。
./routes/index.js
module.exports.fn = require('./not-index.js');
module.exports.user = require('./user.js');

"./routes/not-index.js":
exports.fn = function fn(){};

"./routes/user.js":
exports.user = function user(){};

我们得到输出结果"@routes { user: [Function: user] }"。如果我们将user.js更改为{ ThisLoadedLast: [Function: ThisLoadedLast] },我们得到输出结果"@routes { ThisLoadedLast: [Function: ThisLoadedLast] }"。
但是如果我们修改./routes/index.js...
module.exports.fn = require('./not-index.js');
module.exports.ThisLoadedLast = require('./user.js');

"./routes/not-index.js:"
exports.fn = function fn(){};

"./routes/user.js":
exports.ThisLoadedLast = function ThisLoadedLast(){};

我们得到了 "@routes { fn: { fn: [Function: fn] }, ThisLoadedLast: { ThisLoadedLast: [Function: ThisLoadedLast] } }"。
因此,我建议在您的模块定义中始终使用 module.exports
我不完全理解 Node 内部发生了什么,但如果您能更好地理解这一点,请评论并分享您的见解,我相信会有所帮助。
-- 祝编码愉快

我认为它们过于复杂和令人困惑。它应该是透明和直观的。 - ngungo
我同意。在某些情况下,命名空间可能是有用的,但通常不会成为制约因素。 - Cody

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接