模块模式封装单例的核心是用iife将类定义与实例创建封装在闭包中,对外仅暴露统一接口,确保全局无污染、实例唯一且可懒加载、类型友好,并避免工厂函数破坏单例性。

用模块模式封装单例,核心是把类定义和实例创建都包裹在闭包里,对外只暴露一个干净的接口,彻底隔绝全局变量泄漏。
用 IIFE 封装构造函数与唯一实例
把类定义、私有状态、以及实例初始化全部放在立即执行函数内,返回一个始终指向同一对象的引用:
- 构造函数不挂到
window或globalThis上,避免被意外覆盖或误调用 - 实例在闭包内创建一次,后续调用都复用它,天然保证单例性
- 返回的对象可选择性暴露方法,隐藏内部实现细节(比如私有缓存、配置等)
const Logger = (function() {
class LoggerImpl {
constructor() {
this.logs = [];
this.level = 'info';
}
log(msg) {
this.logs.push(`${new Date().toISOString()} [${this.level}] ${msg}`);
}
getHistory() {
return [...this.logs]; // 返回副本,防止外部篡改
}
}
const instance = new LoggerImpl();
return {
log: (msg) => instance.log(msg),
getHistory: () => instance.getHistory()
};
})();
支持懒加载与配置初始化
有些单例依赖运行时参数(如 API 地址、主题色),可在首次调用时才初始化,兼顾灵活性与性能:
- 内部用标志位(
initialized)和缓存变量控制仅初始化一次 - 暴露一个
init(options)方法供上层按需配置,而非在模块加载时硬编码 - 若未初始化就调用功能方法,自动触发默认初始化或抛出提示
const ConfigService = (function() {
let instance = null;
let initialized = false;
function init(options = {}) {
if (initialized) return;
instance = {
apiUrl: options.apiUrl || '/api',
theme: options.theme || 'light',
timeout: options.timeout ?? 5000
};
initialized = true;
}
return {
init,
getApiUrl: () => {
if (!initialized) init();
return instance.apiUrl;
},
getTheme: () => {
if (!initialized) init();
return instance.theme;
}
};
})();
类型友好:配合 TypeScript 做显式类型导出
纯 JS 的模块模式对类型推导不够友好,TypeScript 下可通过命名空间或类型断言明确接口:
- 定义一个接口描述公开 API,再用
as const或类型断言确保返回值类型准确 - 避免使用
any,让 IDE 和编译器能正确提示方法和属性 - 如果项目用 ES 模块,也可将该 IIFE 封装成默认导出,保持导入方式统一
interface LoggerAPI {
log: (msg: string) => void;
getHistory: () => string[];
}
const Logger = (function(): LoggerAPI {
// ... 实现同上
return {
log: (msg) => instance.log(msg),
getHistory: () => instance.getHistory()
};
})();
避免常见陷阱:别让工厂函数破坏单例性
有人误把“每次调用都返回新实例”的工厂函数当成单例——这恰恰是反模式:
- 不要写
createLogger()这类函数并到处调用,它生成的是多个对象 - 模块顶层必须只产生一个实例,所有导出方法都操作这个实例
- 警惕箭头函数或绑定方法意外捕获不同上下文,导致
this指向错乱
不复杂但容易忽略:模块模式本身不保证单例,关键在于你是否真的只创建了一次实例,并且所有访问路径都复用它。











