exports vs module.exports

Node.js 的 exports 和 module.exports 两者之间的联系。

相同之处

exports 和 module.exports 并不是全局变量,而只是对各自的 module 可见。

它们指向同一个对象,其缺省初始值为空 {}。

如果 exports 和 module.exports 没有被重新赋值,这个对象就是将要输出的对象。

不同之处

module 是对当前模块的一个引用。真正输出的是 module.exports。

所以对 exports 直接赋值没有作用。而对 module.exports 直接赋值后,module.exports 就指向新的对象了,这个新的对象成为将要被输出的对象。

详细说明

exports 和 module.exports 指向的是同一个对象,所以给他们任何一个添加属性或方法,另外一个都会接收到变化,因为他们指向的是同一个对象。例如:

exports.afunc = function(){};
module.exports.name = "Wang";
console.log(exports);
console.log(module.exports);

后面两条语句的输出都是: { afunc: [Function], name: 'Wang' }

但是如果是对其中任何一个直接赋值,就会切断对最初对象的引用。

  • exports

例如给 exports 直接赋值,就切断了 exports 对之前其和 module.exports 一同指向的对象的引用,但是由于真正输出的是 module.exports 指向的对象,所以对 exports 赋值无效,比如在 ep.js 输入:

exports.name = "Wang";
var afunc = function(){};
exports = afunc; //注意对 require 无效
console.log(exports); //内部有效,输出 [Function]

在另外一个 me.js 输入:

var ep = require('./ep');
console.log(ep);//输出 {name: 'Wang'},不是 [Function]

要让 exports 输出,可以在 ep.js 加上

module.exports = exports; 

两者的 reference 关系再次建立。这样输出都是 [Function] 。

  • module.exports

如果给 module.exports 直接赋值,也切断了 exports 的引用,同样道理,因为真正输出的是 module.exports 指向的对象,所以新赋给 module.exports 的对象将被输出。例如把 ep.js 改为:

exports.name = "Li";
console.log(exports); //{name: 'Li'}
console.log(module.exports);//一样 {name: 'Li'}
module.exports = "Zhou";
console.log(exports); //还是 {name: 'Li'}
console.log(module.exports);//变为 Zhou

再次运行 me.js,输出 Zhou , 覆盖了之前的对象.

总结

真正输出总是 module.exports。如果两者同时出现或被修改,只有 module.exports 返回,exports 被忽略。

exports 其实是 module.exports 的一个别名 alias,或者说是简写,目的是为了简化模块内的代码。如果只是添加方法和属性那么使用 exports 就没有问题,但是要新建一个对象就得使用 module.exports.

(2015-4-16)补充:

看了朴灵的《深入浅出 Node.js》之后,结合 Node node.js 源码 里的

NativeModule.wrap = function(script) {
  return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

NativeModule.prototype.compile = function() {
  var source = NativeModule.getSource(this.id);
  source = NativeModule.wrap(source);

  var fn = runInThisContext(source, { filename: this.filename });
  fn(this.exports, NativeModule.require, this, this.filename);

  this.loaded = true;
};

可见,Node.js 确实把普通的 JavaScript 文件像如下包了起来:

(function (exports, require, module, __filename, __dirname) { 
    // 你的 js 文件
});

这样每个模块之间实现了作用域的隔离。

然后对于本文想要阐明的 exports vs module.exports, 在那个 wrapper 函数头里,exports 和 module 都是通过形参的方式传入的,有一定编程经验的都知道,在函数里直接赋值给形参会改变形参的引用,但是不会改变函数外传入的值, 但是如果是改变形参的属性或者方法就可以里外都改变。所以就有以上总结的结论了。

参考: