如果你学过 Node.js大概率见过这种面试题exports.name张三module.exports{age:18}请问最后别人require这个文件时拿到的是什么很多人第一反应是两个都导出了吧但答案是只会拿到{age:18}为什么今天我们就从这个面试题开始一步步走到 CommonJS 的底层原理把module.exports和exports的关系讲清楚。先看最常见的写法在 Node.js 里一个文件就是一个模块。比如我们有一个user.jsconstname张三constage18module.exports{name,age}另一个文件里使用它constuserrequire(./user)console.log(user.name)// 张三console.log(user.age)// 18这就是 Node.js 里的 CommonJS 模块规范。CommonJS 主要有两个核心动作module.exports...负责导出。require(...)负责导入。exports是什么除了module.exports你可能还见过这种写法exports.name张三exports.age18它也可以正常导出constuserrequire(./user)console.log(user)// { name: 张三, age: 18 }所以很多人会以为exportsmodule.exports这句话在一开始确实是对的。但注意是“一开始”。Node.js 背后偷偷包了一层函数我们平时写一个文件比如user.jsconstname张三exports.namename看起来代码就是直接执行的。但 Node.js 在执行这个文件之前会给它外面包一层函数大概像这样(function(exports,require,module,__filename,__dirname){constname张三exports.namename})也就是说每个 CommonJS 模块在执行时其实都会被放进一个函数里面。这个函数有 5 个参数exports require module __filename __dirname所以你才能在每个 Node.js 文件里直接使用这些变量。比如console.log(__filename)// 当前文件的完整路径console.log(__dirname)// 当前文件所在目录它们不是全局变量而是 Node.js 帮你传进来的参数。重点来了exports只是module.exports的一个引用Node.js 在执行模块时大概会先准备一个对象constmodule{exports:{}}然后让exports指向它constexportsmodule.exports也就是说刚开始的时候它们俩指向同一个对象。你可以想象成这样module.exports --- {} exports ---^它们指向同一个“房子”。所以你这样写是可以的exports.name张三本质上是在给这个共同的对象加属性module.exports.name张三所以最后导出的结果是{name:张三}为什么exports {}不行很多人会写出这样的代码exports{name:张三,age:18}然后发现导出失败了。为什么因为这相当于让exports这个变量指向了一个新对象module.exports --- {} exports --- { name: 张三, age: 18 }原本exports和module.exports住在同一个房子里。你现在不是装修原来的房子而是自己搬去了新房子。但 Node.js 最后真正拿出去给别人用的是module.exports不是exports所以你给exports重新赋值Node.js 根本不管。最后别人require到的还是原来的空对象{}那为什么module.exports {}可以因为 CommonJS 最终导出的就是module.exports。所以你这样写module.exports{name:张三,age:18}意思是直接把最终要导出的东西换成一个新对象。这当然可以。换句话说exports.xxxxxx是在“给默认导出对象加属性”。而module.exportsxxx是在“直接指定最终导出的内容”。回到开头的面试题来看这段代码exports.name张三module.exports{age:18}第一行exports.name张三此时相当于给默认导出对象加了一个属性module.exports.name张三但第二行module.exports{age:18}直接把module.exports换成了一个新对象。所以最后导出的结果是{age:18}name没了。因为它加在了旧对象上而最终导出的是新对象。再看几个容易混的例子例子一可以exports.name张三exports.age18结果{name:张三,age:18}因为没有断开exports和module.exports的引用关系。例子二不可以exports{name:张三}结果{}因为你只是让exports指向了一个新对象但最终导出的还是module.exports。例子三可以module.exports{name:张三}结果{name:张三}因为最终导出的就是module.exports。例子四容易踩坑module.exports.name张三exports{age:18}结果{name:张三}exports { age: 18 }不会影响最终导出结果。例子五后面覆盖前面exports.name张三module.exportsfunctionsayHello(){console.log(hello)}结果导出的是函数functionsayHello(){console.log(hello)}因为module.exports ...直接改掉了最终导出内容。用生活例子理解可以把module.exports想象成公司前台最后要交给客户的文件袋。一开始exports和module.exports都指向同一个文件袋。你写exports.name张三就像往这个文件袋里放了一张纸纸上写着name 张三。没问题客户最后能看到。但你写exports{name:张三}就像你自己又拿了一个新文件袋把东西放进新文件袋里。问题是前台最后交给客户的还是原来那个module.exports文件袋。所以客户看不到你新文件袋里的东西。而你写module.exports{name:张三}就相当于直接告诉前台最后就把这个新文件袋交给客户。这当然有效。CommonJS 的加载流程大概是什么当你写constuserrequire(./user)Node.js 大概会做几件事根据路径找到user.js创建一个module对象初始化module.exports {}把文件代码包进函数里执行执行过程中你可以修改module.exports最后require返回module.exports伪代码大概是这样functionrequire(filename){constmodule{exports:{}}constexportsmodule.exports;(function(exports,require,module,__filename,__dirname){// 这里执行你的模块代码})(exports,require,module,filename,dirname)returnmodule.exports}这段伪代码不用死背但它能解释大部分 CommonJS 的问题。尤其是这句returnmodule.exports最终返回的是module.exports不是exports。最佳实践实际开发里建议这样记如果你只是导出多个属性可以写exports.name张三exports.age18exports.sayHellofunction(){}或者更直观一点module.exports.name张三module.exports.age18如果你要导出一个整体比如一个对象、函数、类建议直接写module.exports{name:张三,age:18}或者module.exportsfunctionsayHello(){console.log(hello)}不建议这样写exports{name:张三}因为它不会改变最终导出结果。一句话记住区别最重要的一句话CommonJS 最终导出的是module.exports而exports只是module.exports的一个快捷引用。所以exports.xxxxxx可以。exportsxxx通常不可以。module.exportsxxx可以。最后总结CommonJS 里每个文件都是一个独立模块。Node.js 执行模块时会给文件外面包一层函数并传入exports,require,module,__filename,__dirname其中exports一开始只是module.exports的一个引用。所以给exports添加属性是可以的exports.name张三但直接给exports重新赋值通常没用exports{}因为 CommonJS 最后真正返回的是module.exports记住这句话基本就不会再被module.exports和exports绕晕了exports是辅助写法module.exports才是真正的出口。