闭包单例更轻量因其无需类定义、new或static,仅靠作用域封闭与一次求值实现;适合工具函数等无继承需求场景,但模块重载时会失效,且无法跨模块共享。
闭包单例为什么比 class + static 实现更轻量
因为不需要类定义、不依赖
或
关键字,也不触发原型链或构造函数调用,纯粹靠作用域封闭和一次求值。适合工具函数、配置管理器、日志实例等无需继承或生命周期钩子的场景。
关键点在于:闭包内变量只初始化一次,后续调用直接返回缓存值,且外部无法绕过该逻辑访问原始变量。
函数必须严格控制初始化时机
常见错误是把实例创建写在闭包外层立即执行,导致模块加载时就初始化(哪怕没人用),或者误用
却没加锁,在多线程/异步环境下可能重复初始化(虽然 JS 单线程,但 Promise 微任务中仍可能被并发调用)。
正确做法:把实例声明和创建都放在
内部,用
声明,首次调用才执行构造逻辑
避免使用
—— 变量提升会导致作用域污染
如果构造成本高(如读文件、连数据库),可加
缓存,但需注意
后续调用要复用同一个 promise,不能每次 new 一个
闭包单例在模块热更新(HMR)或多次 require 时会失效
Node.js 中反复
或 Webpack HMR 触发模块重载,会导致闭包重新执行,
被重置为
,新调用又生成新实例 —— 看似单例,实则每个模块副本都有自己的“单例”。
解决办法只有两个:
确保该模块在整个应用生命周期中只被引入一次(比如统一在入口文件
,其他地方用导入对象)
改用 WeakMap + 全局注册表(如
),但这就脱离了纯闭包范畴,也增加了全局污染风险
多数前端项目用 ES 模块天然满足“一次加载”,但 SSR 或测试环境里
或
清理容易打破这个前提。
和
全局注册方式的本质区别
闭包单例靠作用域隔离,
靠全局注册表。前者无法跨模块共享,后者可以但需要手动管理键名冲突和清理。
比如你写了一个库,导出的是闭包单例函数,使用者在自己模块里调用两次
是同一个实例;但如果他分别
两份不同路径下的同一份代码(比如 symlink 或 pnpm 的硬链接),就可能得到两个独立闭包 —— 这时候你以为的“全局唯一”其实只是“本模块内唯一”。
真正需要跨模块强一致单例,闭包不是首选;它更适合封装“本模块内应唯一”的状态,比如某个 React 自定义 Hook 内部的缓存控制器。
newstaticgetInstanceif (!instance)getInstancelet instancevarPromiseawaitconst createLogger = () => {
let instance;
return () => {
if (!instance) {
instance = { log: (msg) => console.log(`[LOG] ${msg}`) };
}
return instance;
};
};
const getLogger = createLogger();
require('./logger')instanceundefinedimportglobalThis.loggerInstancejest.mockrequire.cacheSymbol.forSymbol.for('logger')getLogger()import