Menu

Node.js module.exports 与 exports 的区别及示例

module.exports 是一个 CommonJS 文件交给 require() 它的调用者的值。exports 是指向同一个对象的快捷方式,直到你给它重新赋值为止。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

module.exports 是一个 Node.js 文件交给其他用 require() 加载它的文件的值。它一开始是一个空对象:给它添加属性可以导出多个东西,给它赋一个新值(一个函数、一个类)则只导出一个东西。exports 是一个指向同一个对象的简写变量,所以 exports.add = ... 有效,但 exports = ... 会切断这个联系,什么也导出不了。

它打印了 5 20 3.14159,然后是 [ 'add', 'multiply', 'PI' ]。math.js 中的三行都是在给同一个对象添加属性,无论是通过 module.exports 还是 exports,而 require('./math') 返回的正是这个对象。

导出一个函数、类或对象

当模块只有一个主要内容时,直接把它赋给 module.exports。这样调用者拿到的就是这个函数本身,而不是包着它的对象:

它打印了 Hello, Ada 和 Grace (user)。如果你看到 TypeError: greet is not a function,说明调用者用错了形式:对象属性要写 const { greet } = require(...),直接赋的值要写 const greet = require(...)。

module.exports 与 exports:重新赋值的陷阱

在你的文件运行之前,Node.js 会先设置 exports = module.exports。两个名字指向同一个对象,而 require 返回的是 module.exports。给 exports 赋一个新值,只会改变这个局部变量:

broken.js 导出的是 {},因为 exports 现在指向一个新对象,而没有任何东西返回它。working.js 导出的是 { version: 2 }。

写法能导出吗?
module.exports.x = 1能
exports.x = 1能,是同一个对象
module.exports = { x: 1 }能,替换了整个对象
exports = { x: 1 }不能,这个值丢失了

最简单的规则:永远写 module.exports。它在任何情况下都有效,代码也能清楚地表达它在做什么。

require 返回同一个缓存对象

Node.js 只在模块第一次被 require 时运行一次它的代码,并缓存 module.exports。之后对同一个文件的每次 require 都返回同一个对象,所以模块内部的状态由所有加载它的文件共享:

counter.js is running 只出现了一次,b.next() 返回 3,a === b 为 true,因为缓存的键是解析后的文件路径,而 ./counter 和 ./counter.js 解析到的是同一个文件。这就是为什么在一个模块中创建的数据库连接或配置对象会在整个应用中共享。

module.exports 与 ES 模块

ES 模块改用 export 和 import。两种写法的对应关系如下:

// CommonJS                              // ES modules
module.exports = greet;                  export default greet;
module.exports.add = add;                export { add };
const greet = require('./greet');        import greet from './greet.js';
const { add } = require('./math');       import { add } from './math.js';

ES 模块可以 import 一个 CommonJS 文件:它的 module.exports 会作为默认导入传过来。从 Node.js 22.12 开始,require() 也能加载没有顶层 await 的 ES 模块。什么时候选哪一种,请看 CommonJS 与 ES 模块。

常见错误

在同一个文件里混用 exports.x 和 module.exports = ...。 赋值会替换整个对象,所以在它之前写的每个 exports.x 都没了。每个文件只选一种写法。

导出一个之后会变化的值。 module.exports = { count } 在加载时复制了这个数字;当模块更新 count 时,调用者看到的仍是旧值。像上面的 counter.js 那样,导出一个返回该值的函数。

漏写了 ./。 require('math') 会去 node_modules 里找一个包,而不是找你文件旁边的 math.js。本地文件总是以 ./ 或 ../ 开头。

常见问题

可以在同一个文件里同时使用 module.exports 和 export 吗?

不可以。一个文件要么是 CommonJS(require、module.exports),要么是 ES 模块(import、export)。Node.js 根据扩展名(.cjs 或 .mjs)或 package.json 中的 "type" 来判断。

为什么 require 返回一个空对象?

通常是因为模块写的是 exports = ... 而不是 module.exports = ...,或者两个模块互相 require。在循环 require 中,其中一个模块会在另一个文件还没运行完时就拿到它的 module.exports。

require 时需要写 .js 扩展名吗?

不需要。require('./math') 会依次尝试 ./math、./math.js、./math.json 和 ./math.node,然后是带有 index.js 的文件夹 ./math。在 ES 模块的 import 中,扩展名是必需的。

如何重新加载一个已被 require 缓存的模块?

用 delete require.cache[require.resolve('./math')] 删除它的缓存条目,然后再 require 一次。这主要在测试和开发工具中有用;应用代码不应该依赖它。

Coddy 编程语言插图

和 Coddy 一起学编程

开始