在我们继续之前,请务必了解how modules are actually loaded by node。
从节点的模块加载系统中删除的关键是在它实际运行代码之前require(发生在Module#_compile),它creates 一个新的空exports 对象作为属性Module 的。 (换句话说,你的可视化是正确的。)
节点然后wrapsrequired 文件中带有匿名函数的文本:
(function (exports, require, module, __filename, __dirname) {
// here goes what's in your js file
});
...基本上是evals 那个字符串。 evaling 该字符串的结果是一个函数(匿名包装器),节点立即调用该函数,传入如下参数:
evaledFn(module.exports, require, module, filename, dirname);
require、filename 和dirname 是对require 函数(实际上是isn't a global)的引用,以及包含已加载模块路径信息的字符串。 (这就是docs 的意思,当他们说“__filename 实际上不是全局的,而是每个模块的本地。”)
所以我们可以在模块内部看到,确实是exports === module.exports。但是为什么模块会同时获得module 和exports?
向后兼容性。在节点的早期,模块内部没有module 变量。您只需分配给exports。但是,这意味着您永远不能将构造函数导出为模块本身。
一个熟悉的模块将构造函数导出为模块的示例:
var express = require('express');
var app = express();
这是因为Express exports a function by reassigning module.exports,覆盖了node默认给你的空对象:
module.exports = function() { ... }
但是,请注意,您不能只写:
exports = function { ... }
这是因为JavaScript's parameter passing semantics are weird。从技术上讲,JavaScript 可能被认为是“纯按值传递”,但实际上参数是“按值引用”传递的。
这意味着当您将对象传递给函数时,它会接收对该对象的引用作为值。您可以通过该引用访问原始对象,但您不能改变调用者对引用的看法。换句话说,没有像您在 C/C++/C# 中看到的那样的“out”参数。
作为一个具体的例子:
var obj = { x: 1 };
function A(o) {
o.x = 2;
}
function B(o) {
o = { x: 2 };
}
调用A(obj); 将导致obj.x == 2,因为我们访问的是作为o 传入的原始对象。但是,B(obj); 不会做任何事情; obj.x == 1。将一个全新的对象分配给B 的本地o 只会更改o 指向的内容。它对调用者的对象obj 没有任何作用,仍然不受影响。
现在应该很明显为什么有必要将module 对象添加到节点模块的本地范围。为了允许模块完全替换exports 对象,它必须作为传递给模块匿名函数的对象的属性可用。显然没有人想破坏现有的代码,所以 exports 被留下作为对 module.exports 的引用。
因此,当您只是为导出对象分配属性时,使用exports 或module.exports 并不重要;它们是相同的,指向完全相同的对象的引用。
仅当您要将函数导出为顶级导出时,您必须使用module.exports,因为正如我们所见,只需将函数分配给exports在模块范围之外没有任何影响。
最后一点,当您将函数导出为模块时,最好将exports 和module.exports 分配给两者。这样一来,两个变量都保持一致,并且与它们在标准模块中的工作方式相同。
exports = module.exports = function() { ... }
请务必在模块文件顶部附近执行此操作,以免有人意外分配给最终被覆盖的 exports。
另外,如果您觉得这很奇怪(三个 =s?),我正在利用包含assignment operator 的表达式返回分配值的事实,这使得可以一次将单个值分配给多个变量。