前言
exports和module.exports这两个之间的关系一直傻傻的分不清,为啥有了module.exports还要有exports?我想通过这篇文章来理清两者之间的关系。
引用类型的形参
在说这两个之前,想先说一个知识点,当 JS 函数参数是引用类型时,其形参在函数内的改变对原变量的影响,这也是理解exports和module.exports关系的关键。
举两个例子:
例 1:
var myInfo = {name: 'Robbie'};
var changeInfo = function (info) {
info.age = '18';
console.log('info: ', info);
};
changeInfo(myInfo);
console.log('myInfo: ', myInfo);
// info: {name: "Robbie", age: 18}
// myInfo: {name: "Robbie", age: 18}
例 2:
var myInfo = {name: 'Robbie'};
var changeInfo = function (info) {
info = {name: 'Robbie', age: 18};
console.log('info: ', info);
};
changeInfo(myInfo);
console.log('myInfo: ', myInfo);
// info: {name: "Robbie", age: 18}
// myInfo: {name: "Robbie"}

图:例1(上)和 例2(下)操作示意图
如上图,例 1 很简单,因为myInfo是引用类型,传递的是堆内存中的内存地址,info和myInfo两者指向同一块地址空间,所以向info中添加元素时,myInfo读到的是同一份数据。
在例 2 中函数内部对info进行了重写,为它开辟了新的地址空间,所以info和myInfo在指向的完全是两块内容。
exports 和 module.exports 的关系
在 CommonJS 模块规范中,每个模块文件中都存在着require、exports、module 这 3 个变量。模块导出一般常用的就是exports或者module.exports。
我们在一些资料上经常会看到下面这句话:
在 CommonJS 模块规范中
exports实际上是对module.exports的引用
引用?什么意思?你说引用就引用呀,怎么引用的?
为了弄清楚exports到底是如何引用module.exports的,我们尝试用下面的例子进行探索。
// lib.js
exports.info = {name: 'Robbie', age: 18};
console.log('module.exports: ', module.exports);
console.log('exports: ', exports);
module.exports = function () {
console.log('robbie');
};
console.log('---修改后---');
console.log('module.exports: ', module.exports);
console.log('exports: ', exports);
// index.js
var info = require('./lib.js');
console.log('-----');
console.log('require: ', info);
上面的 index.js 文件中导入了 lib 文件,执行node index.js后,打印如下
module.exports: { info: { name: 'Robbie', age: 18 } }
exports: { info: { name: 'Robbie', age: 18 } }
---修改后---
module.exports: function () {
console.log('robbie')
}
exports: { info: { name: 'Robbie', age: 18 } }
-----
require: function () {
console.log('robbie')
}
通过例子可以看出,当我们在模块中向exports中添加元素时,module.exports确实也会添加元素,在对module.exports重写后,两者指向的内容不同,可以证明两者确实存在引用关系。
通过 index 文件中打印的内容可以看出,对于模块引入来说,require一个模块其实读的是模块的module.exports指向的内容,不一定是exports(两者指向不同时)。
结合上面两点可以看出,exports实际上就是对module.exports的引用。

图:exports 与 module.exports 关系示意图
也就是说,exports与module.exports指向同一块地址空间。在模块中添加module.exports.变量A = A与exports.变量A = A是完全等价的操作。
赋值引用还是函数传参引用
其实上面通过案例已经说明白这两者之间的关系了,此处通过webpack对上面定义的lib模块进行了转义,截取了其中的一段代码,简单看一下node模块的封装。
(function (modules) {
function __webpack_require__(moduleId) {
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {},
});
// Execute the module function
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
// Return the exports of the module
return module.exports;
}
return __webpack_require__('/index.js');
})({
'/index.js': function (module, exports, __webpack_require__) {
var info = __webpack_require__('/lib.js');
console.log('-----');
console.log('require: ', info);
},
'/lib.js': function (module, exports) {
exports.info = {
name: 'Robbie',
age: 18,
};
console.log('module.exports: ', module.exports);
console.log('exports: ', exports);
module.exports = function () {
console.log('robbie');
};
console.log('---修改后---');
console.log('module.exports: ', module.exports);
console.log('exports: ', exports);
},
});
通过module变量的定义可以看出,exports的本质是module变量中定义的一个对象。模块执行时,通过函数引用类型传参的方式将module作为函数的第一个参数传递给模块函数,module.exports作为函数的第二个参数传递给模块函数。所以我们在导出的时既能用module.exports,也可以用exports。
本文小结
和文章开头的例子对比一下,有没有感觉很像。exports就是那个形参,是对module.exports的引用。如果不对两者重新定义,只向其中添加元素,这两个其实是“完全等价”的。
而对于模块的引用来说,require一个模块读取的是module.exports所指向的内容。所以我们在导出模块的内容时可以使用exports.变量A,但最好不要两种方式一起用,更不要在混用的同时,还对其中的一个进行重写,这样才能保证导出的内容被require到。
相关拓展
与本文内容相关的还有 ES6 模块的导出规范,如果对 ES6 模块export和export default感兴趣,可以点击此处查看相关总结。
