模块内部未挂载到exports的常量和变量默认不可见,exports仅控制导出边界而非提供安全防护;正确做法是只导出必要接口,私有项留在顶层作用域或用闭包封装。

模块内部的常量和变量不会自动对外暴露,真正需要防范的是“主动导出不当”带来的泄露。exports 本身不是安全屏障,而是导出开关——用对了能守住边界,用错了反而开门揖盗。
明确 exports 的作用边界
exports 是 CommonJS 模块(Node.js 环境)中用于挂载可被 require 访问的成员的对象。它不提供访问控制,只决定“哪些东西能被其他文件拿到”。关键点在于:
- 未赋值给 exports 的变量、函数、常量,默认完全不可见,连同模块作用域一起被隔离
- 只要写成 exports.xxx = ... 或 module.exports.xxx = ...,就等于主动打开一个访问入口
- 直接赋值 exports = {...} 会切断与 module.exports 的引用关系,导致模块整体不可导入——这是常见误操作,不是私有化手段
避免私有常量被意外导出的写法
假设你有一个配置模块 config.js,含敏感 API 密钥和仅供内部使用的辅助常量:
- ❌ 错误:把私有常量也挂到 exports 上
exports.API_KEY = 'sk_live_...';
exports.DEBUG_MODE = true;
exports._INTERNAL_PREFIX = 'v2_'; // _ 命名约定无效,仍可被外部读取 - ✅ 正确:仅导出真正需要的接口,私有项留在模块顶层作用域
const API_KEY = 'sk_live_...';
const _INTERNAL_PREFIX = 'v2_';
exports.getApiUrl = () => `https://api.example.com/${_INTERNAL_PREFIX}`;
exports.isProduction = () => process.env.NODE_ENV === 'production';
这里 API_KEY 和 _INTERNAL_PREFIX 从未出现在 exports 上,外部模块即使 require 进来,也无法通过任何方式访问它们。
用闭包封装敏感数据(进阶防护)
当需要导出一个对象或类,又必须隐藏其内部状态时,仅靠不挂 exports 不够——因为对象属性可能被外部修改。此时应主动构造封闭环境:
- 工厂函数模式:
const createClient = (apiKey) => {
return {
fetch: (url) => fetch(url, { headers: { 'Authorization': `Bearer ${apiKey}` } })
};
};
exports.createClient = createClient;
调用方只能拿到返回对象,无法触及 apiKey 参数值 - ES2022+ 私有字段(若目标环境支持):
class ApiClient {
#apiKey;
constructor(key) { this.#apiKey = key; }
fetch(url) { /* 使用 this.#apiKey */ }
}
exports.ApiClient = ApiClient;
配合构建工具做二次确认
虽然 tree-shaking 不影响运行时可见性,但可作为泄露检查的辅助手段:
- 在 Webpack/Rollup 中启用 usedExports: true,再配合 sideEffects: false,未被 exports 暴露且未被模块内任何导出项引用的常量,会被标记为“未使用”
- 结合 ESLint 规则(如 no-unused-vars + import/no-unused-modules),可提前发现“声明了却没导出也没用”的冗余私有变量,降低维护风险
- 注意:这不能替代逻辑设计,只是工程化兜底

















