JavaScript 中 CommonJS 怎么在 Node.js 中使用 require 和 module.exports 导入导出

唐可
唐可
发布时间:2026-08-25| 62人浏览过
原创

前言

在 Node.js 项目里,CommonJS 看起来只有 require() 和 module.exports 两个入口,但真正容易出错的,往往是“导出的是单个值还是对象”“exports 到底能不能直接赋值”以及和 ESM 混用时的边界。本文按导出、导入、引用关系和常见坑拆开说明,帮你快速判断哪种写法可用、哪种写法会让模块结果与预期不一致。

在 Node.js 项目里,很多老代码和大量现有包仍然建立在 CommonJS 之上。真正容易让人混淆的,并不是 require() 和 module.exports 这两个名字本身,而是它们在“导出单个值、导出多个成员、使用 exports 简写、切换到 ESM”这些场景下的行为差异。下面按常见使用路径拆开讲,你可以直接对照代码判断自己该用哪种写法、哪些写法会踩坑。

CommonJS 在 Node.js 里到底做什么

CommonJS 是 Node.js 默认的模块系统。每个 .js 文件都可以看成一个独立模块,模块内部有自己的私有作用域,外部不能直接访问文件里的变量、函数或常量。

模块之间要通信,核心就两件事:

  • 用 module.exports 把内容显式暴露出去
  • 用 require() 把别的模块加载进来

它的特点是语法直接、运行时生效,特别适合传统的 Node.js 服务端开发场景。理解这一点后,后面的导入导出规则就会清晰很多:外部拿到的始终是目标模块最终的 module.exports。

怎么用 module.exports 导出内容

如果一个模块想对外提供能力,必须通过 module.exports 导出。它可以导出单个值,也可以导出一个对象,甚至可以导出任意类型。

用纸张卡片展示 module.exports 导出单个值与多个成员的区别
导出单值和导出对象,导入方式为什么不同把“导出单个值”和“导出对象成员”放到同一张专栏配图里,读者更容易对照导入方式理解结构差异。

1. 导出单个函数或类

当一个文件只打算提供一个主要能力时,最直接的写法就是把这个值整体赋给 module.exports。导入方拿到的也就是这个值本身。

// utils.js
const helper = (x) => x * 2;
const VERSION = "1.0";

// 方式一:导出单个函数
module.exports = helper;

// 方式二:导出多个成员(推荐常用)
// module.exports = { helper, VERSION };

2. 导出多个成员

如果一个模块需要同时暴露多个函数、常量或配置项,更常见的做法是导出一个对象:

用便签关系图解释 exports 与 module.exports 的引用关系和错误重赋值
为什么给 exports 重新赋值会失效这一节最容易踩坑,适合用“同指向”和“断开引用”两种状态做可视化说明。
  • 对象的属性名,就是导入后可访问的键名
  • 适合工具库、配置模块、聚合模块这类场景

例如上面的注释写法:

module.exports = { helper, VERSION };

这样导入后就可以通过对象属性或解构来使用。

3. CommonJS 不限制导出类型

module.exports 并不要求你只能导出对象。函数、对象、数组、字符串、数字,甚至 undefined 都可以导出。也正因为如此,导入方必须先知道对方导出的到底是什么结构,否则很容易把“默认拿到一个函数”和“拿到一个对象后再解构”这两种情况混在一起。

怎么用 require 导入模块

require() 的作用是加载目标模块,并返回该模块的 module.exports。它是同步执行的,也就是说调用发生时,Node.js 会立刻去加载并执行目标模块代码。

// app.js
const helper = require('./utils.js'); // 若 utils.js 导出的是函数
console.log(helper(5)); // 输出 10

// 若 utils.js 导出的是 { helper, VERSION }
// const { helper, VERSION } = require('./utils.js');

路径怎么写

  • 相对路径必须以 ./ 或 ../ 开头
  • 绝对路径可以从磁盘根目录开始,但实际项目里不常用
  • 如果写的是包名,比如 lodash,Node.js 会去 node_modules 中查找

扩展名能不能省略

可以。Node.js 会按顺序尝试这些扩展名:

  • .js
  • .json
  • .node

不过从代码可读性出发,团队里如果希望路径更明确,显式写出 .js 也完全合理。

为什么多次 require 不会重复执行

这是因为 CommonJS 模块有缓存机制。对同一路径多次调用 require(),模块通常只会在第一次加载时执行一次,后续返回的是缓存结果。

这会带来两个直接影响:

  • 有初始化副作用的模块,不会因重复导入而反复执行
  • 模块内部如果维护状态,多处 require 可能共享同一份结果

exports 和 module.exports 到底有什么区别

这是 CommonJS 里最容易写错的地方。很多人以为 exports 是另一个独立的导出接口,其实不是。

exports 只是 module.exports 的初始引用。也就是说,在模块刚开始执行时,它们指向的是同一个空对象,因此下面这种写法是成立的:

exports.fn = () => {};
exports.num = 42;

这等价于:

module.exports.fn = () => {};
module.exports.num = 42;

问题出在“重新赋值”上。下面这种写法看起来像是在导出一个新对象,实际上会失效:

exports = { a: 1 };

原因是这行代码只是让局部变量 exports 指向了一个新对象,但真正决定模块导出结果的 module.exports 并没有被改掉。于是外部 require() 进来时,拿到的仍然不是你新赋值后的那个对象,甚至可能是空对象。

可以这样记

  • 想导出单个值:用 module.exports = xxx
  • 想导出多个成员:用 module.exports = { a, b }
  • 如果只是给同一个导出对象追加属性:可以写 exports.a = a
  • 不要写 exports = ... 这种重赋值形式

一句话总结就是:最终生效的只有 module.exports,exports 只是它的一个便捷别名。

常见问题与和 ESM 的关系

循环依赖为什么会拿到“不完整对象”

在循环依赖场景下,require() 返回的是目标模块“当前已经执行完成的那部分” module.exports。如果对方模块还没跑完,你拿到的就可能是不完整对象。这也是很多循环依赖 bug 难排查的原因:代码不一定报错,但值可能是半初始化状态。

CommonJS 和 ESM 不能在同一文件里混写

如果项目已经启用 ESM,例如在 package.json 中声明了 "type": "module",那这个文件环境下就不能再直接使用 require 和 module.exports。

同样地,import/export 和 CommonJS 也不该在同一文件里混着写。看到导入导出报错时,先确认项目当前到底跑的是哪一种模块系统,往往比盯着语法本身更重要。

ESM 如何导入 CommonJS 模块

如果你是在 ESM 中使用 CommonJS 模块,可以直接这样写:

import cjs from './cjs-file.js'

这种情况下,CommonJS 模块通常会被自动包装成默认导出。也就是说,ESM 侧拿到的是那个 CommonJS 模块整体导出的值。

实际开发里,最稳妥的判断方式是先看这两个问题:

  • 当前文件所在项目是不是已经启用了 "type": "module"
  • 目标模块导出的究竟是单个值,还是一个对象

把这两点先确认清楚,绝大多数 CommonJS 导入导出问题都能快速定位。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。