之前在面试的时候曾经被问到过关于commonJS规范中module.exports与exports的区别。之前仅仅知道exports是module.exports的一个引用而已,其之间的具体区别没有过多关注。最近整理了一下两者的区别,究竟什么时候两者是相同的,什么时候两者又是不同的。

CommonJS规范中模块的写法以及引用

commonJS规范中定义了模块的概念,其中包含核心模块以及文件模块。为什么需要module.export或者exports的原因在于暴露给外部模块引用本模块时的函数或变量。在外部模块中通过require接口引用其他模块。在被引用模块中通过module.exports或者exports暴露该模块的函数或者变量。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
sum.js

function sum(a,b){
return a+b;
}
module.exports = sum;


main.js

var sum = require(./sum.js);
console.log(sum(1,2));//3

上述示例演示了一个最基本的模块的写法以及引用。

两者之间的联系与区别

module.exports在模块开始的时候是一个空对象{},exports为module.exports的一个辅助工具,其指向module.exports指向的对象。即类似于:

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

这就说明了module.exports与exports指向同一块内存地址。require接口调用的时候以module.exports的值为准。
两者在以下情况下是一致的:

1
2
exports.name = name;
modle.exports.name = name;

两者在以下情况下是有区别的:

1
2
3
4
5
6
7
8
exports = {
name: name,
getName: function(){}
};
module.exports = {
name: name,
getName: function(){}
};

总结:两者在以对象的属性导出的时候是一致的,此时用exports导出和module.exports是一样的效果。如果只使用exports尝试导出一个对象或者function的时候此时会导致不一致的情况。

使用exports导出与module.exports导出出现不一致的原因

之所以用exports与module.exports出现不一致的原因在于:两者开始引用同一块内存地址,如果单纯以对象的属性的方式导出的话(情况一),此时相当于给两者共有的对象添加一个属性并导出。一旦只是用exports导出的是一个function或者一个对象,则exports指向的对象发生了变化,与module.exports
变失去了联系。require接口是以module.exports对象返回的值为依据,因此此时如果仅适用exports导出一个函数或者对象的话会出现错误。下面用一个最简单的示例解释两个变量同时引用一个对象的情况。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var person1 = {
name: 'monster1935',
age: '23'
};

var person2= person1;//情况1

person2.gender = "male";
console.log(person2.gender);//male
console.log(person1.gender);//male

person2 = {//情况2
name: 'monster',
age: "24"
};
console.log(person1.name);//monster1935

上例中情况1说明person2与person1指向的是同一个对象,通过peroson2.gender的方式对两者共同引用的对象添加了一个gender属性。这种情况下打印person1.gender也得到male。情况2中person2重新引用了另外一个新对象,此时person1与person2引用两个不同的对象,两者无联系。这也就解释了使用exports一个对象的时候会出现module.exports不一致的情况。

总结

这个知识点的重点在于对JavaScript中变量引用的对象的理解。JavaScript中对象名仅仅是一个引用。通过对象名之间的赋值,本质上是使两者指向同一块内存上的地址。同样的现象在JavaScript中讲述继承重写原型prototype的时候也曾出现。比如:如果使用Child.prototype.getName=function(){}方式为Child的prototype添加一个方法。这种方式不会重写函数的原型,仅仅是在函数原型的基础上添加一个属性而已。如果使用这种方式:

1
2
3
4
5
6
Child.prototype={
constructor: Person,
getName:function(){

}
};

这种方式相当于重写了prototype原型。prototype原型重新指向了另外一个新的对象,一般需要在这种情况下的继承需要显示的指明constructor属性以达到标明对象类型的目的。


参考文章:
https://cnodejs.org/topic/5231a630101e574521e45ef8