单例模式核心解析
- 单例模式是一种创建型设计模式,核心目标是保证一个类在程序生命周期内只有一个实例,并提供一个全局访问点。简单来说,无论多少次尝试创建这个类的实例,最终得到的都是同一个对象,避免重复创建导致的资源浪费;
- 单例模式核心特征
- 私有静态实例:类内部维护一个唯一的实例对象,外部无法直接访问;
- 私有构造函数:防止外部通过 new 关键字直接创建新实例;
- 公有静态方法:提供全局访问点,用于获取 / 创建唯一实例;
- 类图:
- 实现代码:
// 饿汉式单例 - 立即创建实例 class SingletonEager { // 静态实例 - 类加载时就创建 static instance = new SingletonEager(); // 私有构造函数(JS中无法真正私有,但用约定表示) constructor() { if (SingletonEager.instance) { throw new Error('单例类不能直接实例化,请使用 getInstance() 方法'); } // 初始化属性 this.createdAt = new Date(); console.log('饿汉式单例实例被创建'); } // 静态方法获取实例 static getInstance() { return SingletonEager.instance; } } // 测试饿汉式单例 console.log('=== 饿汉式单例测试 ==='); try { const instance1 = SingletonEager.getInstance(); const instance2 = SingletonEager.getInstance(); console.log('实例是否相同:', instance1 === instance2); // true // 尝试直接实例化 // const instance3 = new SingletonEager(); // 抛出错误 } catch (error) { console.error('错误:', error.message); }// 懒汉式单例 - 需要时才创建 class SingletonLazy { // 静态实例引用 static #instance = null; // 使用私有字段 // 私有构造函数 constructor() { if (SingletonLazy.#instance) { throw new Error('单例类不能直接实例化,请使用 getInstance() 方法'); } // 初始化属性 this.createdAt = new Date(); console.log('懒汉式单例实例被创建'); } // 静态方法获取实例(延迟加载) static getInstance() { // 双重检查锁定(JS中不需要真正的锁,但保留逻辑) if (!SingletonLazy.#instance) { console.log('首次调用,创建实例'); SingletonLazy.#instance = new SingletonLazy(); } return SingletonLazy.#instance; } } // 测试懒汉式单例 console.log('\n=== 懒汉式单例测试 ==='); console.log('此时实例还未创建'); const lazy1 = SingletonLazy.getInstance(); const lazy2 = SingletonLazy.getInstance(); console.log('实例是否相同:', lazy1 === lazy2); // trueconst SingletonWithClosure = (function () { let instance = null; // 私有实例(对应类图的 #instance) // 私有构造逻辑(对应类图的 constructor) function createInstance() { return { name: "闭包单例示例", businessMethod: () => console.log("执行闭包单例业务逻辑"), }; } // 公有访问方法(对应类图的 getInstance) return { getInstance: () => { if (!instance) { instance = createInstance(); } return instance; }, }; })(); // 测试 const c1 = SingletonWithClosure.getInstance(); const c2 = SingletonWithClosure.getInstance(); console.log("c1 === c2:", c1 === c2); // 输出:true
常用使用场景
场景 1:全局状态管理(如 Vuex/Pinia 核心、自定义全局存储)
-
原理:应用的全局状态需要唯一数据源,避免多实例导致状态不一致;
-
示例:Pinia 的 defineStore 创建的 store 本质是单例,整个应用中同一个 store 只会实例化一次;
场景 2:弹窗 / 模态框(Modal/Dialog)
-
原理:页面中同类型弹窗 (如登录弹窗) 无需重复创建 DOM 节点,复用同一个实例可减少 DOM 操作,提升性能;
-
示例:
// 弹窗单例实现 const ModalSingleton = (() => { let modalInstance = null; function createModal() { const div = document.createElement("div"); div.className = "modal"; div.style.display = "none"; document.body.appendChild(div); return { show: () => (div.style.display = "block"), hide: () => (div.style.display = "none"), setContent: (content) => (div.innerHTML = content), }; } return { getInstance: () => { if (!modalInstance) { modalInstance = createModal(); } return modalInstance; }, }; })(); // 多次调用仅创建一个弹窗 DOM ModalSingleton.getInstance().show(); ModalSingleton.getInstance().setContent("登录成功!");
场景 3:全局缓存管理
-
原理:全局缓存 (如接口数据缓存) 需要唯一的缓存容器,避免多实例导致缓存数据不一致;
-
示例:
const CacheSingleton = { // 单例实例(全局唯一) instance: null, getInstance() { if (!this.instance) { this.instance = { cache: new Map(), set: (key, value) => this.instance.cache.set(key, value), get: (key) => this.instance.cache.get(key), }; } return this.instance; }, }; // 接口请求缓存示例 async function fetchData(url) { const cache = CacheSingleton.getInstance(); if (cache.get(url)) { return cache.get(url); // 复用缓存数据 } const res = await fetch(url); const data = await res.json(); cache.set(url, data); // 存入缓存 return data; }
场景 4:全局事件总线(EventBus)
-
原理:前端组件间通信的事件总线需要唯一实例,否则事件无法跨组件传递;
-
示例:
class EventBus { static #instance = null; constructor() { this.events = new Map(); } static getInstance() { if (!EventBus.#instance) { EventBus.#instance = new EventBus(); } return EventBus.#instance; } on(event, callback) { if (!this.events.has(event)) { this.events.set(event, []); } this.events.get(event).push(callback); } emit(event, data) { if (this.events.has(event)) { this.events.get(event).forEach((cb) => cb(data)); } } } // 组件A订阅事件 EventBus.getInstance().on("user-change", (data) => console.log(data)); // 组件B发布事件(全局可触发) EventBus.getInstance().emit("user-change", { name: "张三" });
单例模式优缺点
优点
-
唯一性保证:确保一个类只有一个实例;
-
全局访问点:提供统一的访问入口;
-
延迟加载:懒汉式实现可以延迟实例化;
-
避免重复创建:节省系统资源;
-
状态共享:多个模块可以共享同一个状态;
缺点
-
违反单一职责:同时负责创建和管理实例;
-
难以扩展:继承困难,测试不便;
-
隐藏依赖:全局状态可能导致耦合;
-
并发问题:多线程环境下需要特殊处理;
-
生命周期管理:实例常驻内存,不易销毁;
工厂模式
上一篇