Skip to main content

exports 和 module.exports

· 7 min read
Robbie Han

前言

exportsmodule.exports这两个之间的关系一直傻傻的分不清,为啥有了module.exports还要有exports?我想通过这篇文章来理清两者之间的关系。

引用类型的形参

在说这两个之前,想先说一个知识点,当 JS 函数参数是引用类型时,其形参在函数内的改变对原变量的影响,这也是理解exportsmodule.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是引用类型,传递的是堆内存中的内存地址,infomyInfo两者指向同一块地址空间,所以向info中添加元素时,myInfo读到的是同一份数据。

在例 2 中函数内部对info进行了重写,为它开辟了新的地址空间,所以infomyInfo在指向的完全是两块内容。

exports 和 module.exports 的关系

在 CommonJS 模块规范中,每个模块文件中都存在着requireexportsmodule 这 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 关系示意图

也就是说,exportsmodule.exports指向同一块地址空间。在模块中添加module.exports.变量A = Aexports.变量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 模块exportexport default感兴趣,可以点击此处查看相关总结

参考资料

Node.js 模块里 exports 与 module.exports 的区别? --- 小明 plus