Node.js模块加载之exports与module.exports的区别详解
你是否曾对Node.js中exports
和module.exports
感到困惑?比如,为什么exports.hello = hello
能够正常导出内容,而exports = hello
却没有效果?今天,咱们就深入探讨一下这其中的缘由。
一、模块初始化时的内部机制
当Node.js加载一个模块时,其底层会进行一系列操作。简单来讲,在模块初始化阶段,会发生以下事情:
// 模块初始化时,Node.js内部创建了一个module对象,其中exports属性初始化为一个空对象 const module = { exports: {} }; // 同时,创建了一个exports变量,它指向module.exports const exports = module.exports;
可以这么理解,module.exports
才是真正用于导出内容的对象,而exports
只是对module.exports
的一个引用。在初始状态下,它们指向同一块内存空间,就像是两个指针同时指向同一个物体,对其中一个操作,另一个也会跟着变化 。
二、常见导出写法的差异分析
在Node.js模块内部,常见的导出写法主要有三种,每种写法背后的内存变化和导出效果都有所不同。
(一)写法一:module.exports = hello
module.exports = hello;
在这种情况下,module.exports
原本指向的对象被替换为hello
函数。此时,module.exports
指向了新的hello
函数,而exports
仍然指向初始化时的那个空对象,但这个空对象已经不再被用于导出内容,因为Node.js最终返回的是module.exports
,所以这种替换操作是有效的,外部可以通过require
获取到hello
函数。
(二)写法二:exports.hello = hello
exports.hello = hello; // 该操作等价于 module.exports.hello = hello
这种写法是给exports
(也就是module.exports
,因为它们开始时指向同一个对象)所指向的对象添加一个属性hello
。此时,module.exports
和exports
仍然指向同一个对象,只是这个对象的结构发生了变化,变成了{ hello: hello函数 }
。由于两个变量指向同一块内存,修改其中一个就相当于修改了另一个,所以这种导出方式也是有效的,外部能够正确获取到导出的hello
属性。
(三)写法三:exports = hello
exports = hello;
这种写法就容易让人产生误解。在这里,exports
被重新赋值,使其指向了hello
。但需要注意的是,module.exports
并没有发生改变,仍然指向初始化时的那个空对象。因为exports
本质上是一个局部变量,对它重新赋值只会改变它自身的指向,并不会影响到module.exports
。而Node.js在加载模块时,最终返回的是module.exports
,所以这种写法无法将hello
正确导出,是无效的。
为了更形象地理解这三种写法的区别,我们可以打个比方。把module.exports
想象成一个真正的快递盒子,它里面装着要发送出去的东西(即导出的内容);而exports
则是一个临时小标签,方便你往盒子里添加物品(即添加导出的属性)。当你使用module.exports = 新内容
时,就相当于直接换了整个快递盒子,接收方自然能收到新的内容;使用exports.xxx = 内容
,就像是往原来的盒子里放了新东西;但要是用exports = 新内容
,就好比只是换了个标签,快递盒子本身并没有改变,接收方收到的还是原来盒子里的东西。
三、Node.js模块执行的内部流程
实际上,Node.js在加载模块时,内部执行的大致流程可以用以下伪代码表示:
function require(modulePath) { // 1. 首先创建module和exports const module = { exports: {} }; const exports = module.exports; // 2. 执行模块代码 (function (exports, module) { // 模块中的代码在这里执行 exports = hello; // 这种操作只是改变了exports的指向,module.exports未受影响,所以无法正确导出 module.exports = hello; // 这种方式正确,将需要导出的内容挂载到了module.exports上 })(exports, module); // 3. 最后返回module.exports return module.exports; }
从这段代码可以看出,exports
只是在模块执行时传入的一个变量,而require()
最终返回的始终是module.exports
。在模块执行过程中,无论怎么修改exports
,只要没有改变module.exports
,最终导出的内容都不会发生变化。这就好比在打包快递时,Node.js给了你一个空盒子(module.exports = {}
)让你往里装东西,结果你中途换了个标签(exports = xxx
),但盒子里并没有实际装入东西,最后送出去的自然还是空盒子。
四、总结与建议
综上所述,exports
只是一个局部变量,它的主要作用是方便开发者往module.exports
中添加属性。而真正用于导出内容的是module.exports
。在实际开发中,一定要注意避免使用exports = xxx
这种方式,因为它无法将内容正确导出。
如果你想要改变整个导出的内容,直接操作module.exports
就可以;如果只是想给模块添加一些属性,使用exports
来操作会更方便。理解了exports
和module.exports
的区别,在Node.js模块开发中,就能更准确地控制导出内容,避免一些不必要的错误。