module.exports / exports / require

本节目标

Node 里每个模块都有 module 对象,module.exports 才是真正「被导出」的东西;exports 只是 module.exports 的一个引用。

// 运行环境:Node.js 16+(保存为 .cjs 直接 node 运行)
// math.cjs —— 一个 CJS 模块
// exports 是 module.exports 的别名(同一块内存)
exports.add = function (a, b) { return a + b }
// 等价于:
// module.exports.add = function (a, b) { return a + b }

// main.cjs —— 引入并使用
const math = require('./math.cjs')
console.log(math.add(1, 2)) // 3

// ⚠️ 经典坑:直接给 exports 赋值会断掉引用
// exports = function(){}   // 错误!exports 不再指向 module.exports
// ✅ 正确:要给整个模块换导出,用 module.exports
module.exports = function sub(a, b) { return a - b }

关键:require('./x') 返回的就是 module.exports 指向的那个对象/函数。exports.xxx = ... 是在「往那个对象上加属性」;exports = ... 只是让局部变量 exports 指向别处,不影响 module.exports。

名词解释

module.exports:Node 里「模块真正对外暴露的东西」。require 调用得到的就是它。exports 是它的快捷别名。要整体替换导出(导出一个函数/类而非对象),必须赋值给 module.exports,不能只动 exports。

require:CJS 的「导入函数」。传入模块路径,返回该模块的 module.exports。它是同步的、运行时的:调用时才加载并执行目标模块,且结果会被缓存(同一进程第二次 require 拿到同一份)。理解 require 返回什么,关键是盯住对方的 module.exports。

课后练习

练习 1:下面 require('./x') 得到什么?

// x.cjs
exports = { a: 1 }
module.exports = { b: 2 }

答案:得到 { b: 2 }。因为 exports = ... 只是改了局部别名,不改变 module.exports;真正导出的是 module.exports,即 { b: 2 }。这是新手最常踩的坑。

练习 2:exports.add = fn 和 module.exports = { add: fn } 两种写法,调用方有区别吗?

答案:调用方无区别,都通过 require('./x').add 访问。区别在「语义」:前者是「往默认导出对象上挂属性」(适合多导出),后者是「整体替换为新对象」。注意不要混用导致前面挂的属性丢失(整体替换会覆盖)。

总结

exports vs module.exports 是 Node 入门第一坑,我的建议简单粗暴:永远用 module.exports,别碰 exports 这个别名。别名存在的唯一理由是「少打几个字」,但它带来的「赋值断引用」坑远大于那点便利。我写 CJS 一律 module.exports = {...} 或 module.exports = fn,从根上避开混乱。理解本质就一句:require 拿的是 module.exports,exports 只是它的影子,影子指歪了不影响本体,但你也别指望改影子能改本体。记牢这点,CJS 的导出你就彻底通透了。