exports本身不定义契约,关键在于明确导出内容、隐藏实现细节并确保调用方仅依赖导出项;不同语言中exports语义差异大,需按场景正确使用。
exports 指令本身不定义变量接口契约,它只是模块系统中用于导出符号的语法载体;真正构建清晰契约的关键,在于**明确导出什么、不导出什么,以及调用方只依赖导出的内容**。不同语言/环境下的 exports 行为差异很大,不能一概而论。
区分语言场景:exports 不是统一概念
Node.js 的 exports 是 module.exports 的快捷引用,仅用于 CommonJS 模块;ES6 的 export 是编译时声明,支持命名导出与默认导出;C++20 的 export 修饰函数/类/命名空间,控制符号可见性;Go 则根本没有 export 关键字,靠首字母大小写隐式导出。用错上下文,契约就从源头失效。
例如在 Node.js 中写:
-
exports = { config: {} }—— 这会切断与 module.exports 的关联,外部 require 得到空对象 -
module.exports = { init, load }—— 才是稳定导出两个函数,调用方可安全解构使用
契约清晰的核心:只导出稳定接口,隐藏实现细节
接口契约不是“把所有东西都扔出去”,而是有意识地收窄暴露面。比如一个用户服务模块:
- 导出
UserService类或工厂函数(稳定构造方式) - 导出
validateEmail工具函数(纯逻辑、无副作用) - 不导出
dbConnection、cacheClient等内部依赖实例 - 不导出
__internalRetryLogic这类带双下划线的临时方法
这样即使内部改用 Redis 替代内存缓存,只要 UserService 的方法签名和行为不变,上层代码完全无需修改。
避免常见契约断裂点
很多模块看似导出了变量,实则悄悄破坏了契约稳定性:
-
导出可变对象引用:如
exports.config = { timeout: 5000 },外部直接改config.timeout = 10000,导致模块行为不可控 -
导出未冻结的构造函数原型:允许外部篡改
User.prototype.destroy,影响所有实例 -
导出路径拼接字符串:如
exports.API_BASE = '/v1',后续升级 v2 就被迫破坏语义兼容
更稳妥的做法是导出只读配置对象:Object.freeze({ version: 'v1', timeout: 5000 }),或封装为 getter 方法。
配合工具强化契约表达
单靠 exports 语法不足以传达意图,需结合工程实践:
- 在 package.json 的
"types"字段指向 TypeScript 声明文件,明确定义输入输出类型 - 用 JSDoc 注释导出成员的用途、参数、返回值,VS Code 等编辑器可实时提示
- 在模块入口文件顶部加注释说明“本模块对外提供以下 3 个能力”,比代码更早建立认知
- CI 流程中运行
npm pkg export-check类脚本,校验导出内容是否符合预设白名单
契约不是写完就结束的事,而是通过导出粒度、文档、类型和自动化检查共同维持的一致预期。











