尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

单例模式:从弹窗管理到全局状态,一个模式搞定

单例模式:从弹窗管理到全局状态,一个模式搞定 从一个小事故说起不知道你有没有遇到过这样的场景用户疯狂点击打开新网页按钮结果屏幕上弹出了十几个一模一样的窗口。又或者你在项目里维护一个全局配置对象结果发现每个模块都new了一个新实例改了这个没改那个最后状态乱七八糟。这些问题背后其实指向同一个根源有些东西整个系统只需要一个。这就是我们今天要聊的单例模式Singleton Pattern。单例模式到底是什么先给个定义确保一个类只有一个实例并提供一个全局访问点。说白了就是这个类你new一百次拿到手的还是同一个对象。它解决的是全局唯一的问题——全局状态管理、资源复用、配置统一都离不开它。单例模式属于 GoF 23 种设计模式中的创建型模式跟工厂模式、建造者模式是邻居。但它的特殊之处在于别的创建型模式关心的是怎么创建它关心的是怎么不创建多个。单例模式的本质不是只能创建一个实例而是提供一个全局访问点且这个访问点背后永远只有一个实例。从一个弹窗管理器开始先看一个最经典的场景页面上有个按钮点击打开一个新窗口。我们当然不希望每次点击都window.open一发不可收拾而是希望有一个统一的弹窗管家来管理这个行为。!DOCTYPE html html langen head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title单例模式打开窗口/title /head body button idopenBtn打开新网页/button script class Popup { static ins; // 静态属性存放单例实例 static getInstance() { if (!Popup.ins) { Popup.ins new Popup(); } return Popup.ins; } open(url) { window.open(url, _blank); } } const a Popup.getInstance(); // 单例方式获取实例替代 new const b Popup.getInstance(); console.log(a b); // true证明是同一个实例 const openBtn document.getElementById(openBtn); openBtn.addEventListener(click, () { a.open(https://www.baidu.com); }); /script /body /html这段代码的核心逻辑其实就三行static getInstance() { if (!Popup.ins) { Popup.ins new Popup(); } return Popup.ins; }你会发现这就是一个惰性初始化Lazy Initialization第一次调用getInstance()时才创建实例之后直接返回缓存的那个。用a b验证一下确实是同一个对象。但问题来了这个实现有个明显的漏洞——Popup.ins是公开的谁都可以直接改Popup.ins null; // 手贱一下单例就没了 Popup.ins new Popup(); // 或者直接给你换个新的对于中级开发者来说我们得把单例这件事做得更可靠一些。更可靠的单例实现方案一闭包 IIFEconst Popup (function () { let instance; // 私有变量外部无法直接访问 class PopupClass { open(url) { window.open(url, _blank); } } return { getInstance() { if (!instance) { instance new PopupClass(); } return instance; } }; })(); const a Popup.getInstance(); const b Popup.getInstance(); console.log(a b); // true这样instance被闭包保护起来外部根本摸不到单例的唯一性就有了保障。方案二ES6 模块天然单例在 ES6 模块体系中模块本身就是单例的——无论被多少地方import模块代码只执行一次导出的对象永远是同一个。// popup.js class Popup { open(url) { window.open(url, _blank); } } export const popupInstance new Popup();// 任何地方使用 import { popupInstance } from ./popup.js; popupInstance.open(https://www.baidu.com);这种方式简单粗暴也是前端工程化中最常用的单例实现方式。你项目里的 Axios 实例、WebSocket 连接、Vuex/Pinia Store本质上都是这么玩的。方案三Symbol 做私有 key如果你想继续用 Class 的写法但又想保护ins不被随意访问可以用 Symbolconst INS Symbol(singleton); class Popup { static getInstance() { if (!Popup[INS]) { Popup[INS] new Popup(); } return Popup[INS]; } open(url) { window.open(url, _blank); } }Symbol作为属性 key 不会被常规遍历发现也不会跟其他属性冲突。不过说实话在 JS 里防君子不防小人真要改还是能通过Object.getOwnPropertySymbols拿到。所以工程实践中方案二ES6 模块才是最主流的选择。单例模式在前端的高频落地场景理论讲完了来看看真实项目中单例模式都在哪里打工。场景一全局状态管理Pinia / Vuex你用 Pinia 定义的 Store本质上就是一个单例。不管在哪个组件里调用useUserStore()拿到的都是同一个实例状态自然就同步了。// stores/user.js import { defineStore } from pinia; export const useUserStore defineStore(user, { state: () ({ name: , token: }), actions: { setUser(user) { this.name user.name; this.token user.token; } } });Pinia 内部保证了同一个 id 的 Store 只会被创建一次——这就是单例模式在框架层面的应用。场景二Axios 实例封装几乎每个前端项目都会封装一个统一的 HTTP 客户端// utils/request.js import axios from axios; const request axios.create({ baseURL: /api, timeout: 10000, }); // 请求拦截器 request.interceptors.request.use(config { config.headers.Authorization localStorage.getItem(token) || ; return config; }); export default request;整个项目共享这一个 Axios 实例好处显而易见拦截器配置一次、Token 注入一处、错误处理统一。如果每个模块都自己axios.create一个那你改拦截器就要改到怀疑人生。场景三WebSocket 连接管理一个页面通常只需要一条 WebSocket 连接。如果每次组件挂载都new WebSocket()服务端可能会因为连接数爆掉而把你拉黑。// utils/socket.js class SocketManager { static instance null; static getInstance() { if (!SocketManager.instance) { SocketManager.instance new SocketManager(); } return SocketManager.instance; } connect(url) { if (this.ws this.ws.readyState WebSocket.OPEN) { return this.ws; // 已连接直接复用 } this.ws new WebSocket(url); return this.ws; } } export default SocketManager;单例模式的阴暗面聊完了好处也得说实话。单例模式在江湖上有个外号叫披着设计模式外衣的全局变量它确实有一些需要警惕的地方。问题一隐藏的耦合单例是全局可访问的这意味着任何模块都可以依赖它。如果滥用你的代码会变成一张谁也理不清的依赖网。问题二难以测试因为单例是全局唯一的在单元测试中很难重置它的状态。你测完一个用例单例的脏数据可能影响下一个用例。问题三违反单一职责原则单例类往往既要管自己的业务逻辑又要管自己只能有一个实例这件事。这在大型项目中会让类的职责变重。设计模式不是银弹单例用好了是全局状态管理利器用不好就是隐藏的全局变量灾难。什么时候该用单例给你一个简单的判断标准✅ 资源本身在业务上就是唯一的如全局配置、日志收集器、数据库连接池、WebSocket 连接✅ 创建成本高且可以复用如线程池在 JS 中更多是连接池、缓存管理器❌ 仅仅因为方便就把普通对象改成单例比如用户信息、购物车数据这些应该交给状态管理工具Pinia/Vuex/Redux而不是手动实现单例总结这篇文章从一个弹窗管理的例子出发聊了单例模式的核心思想、多种实现方式以及在前端工程中的真实应用场景。来一张图帮你快速回顾实现方式适用场景私有性Class 静态方法简单场景、教学演示❌ 属性公开闭包 IIFE需要保护实例引用✅ 私有变量ES6 模块导出实例工程实践首选✅ 天然单例Symbol 属性需要 Class 写法 一定保护⚠️ 半私有核心就一句话确保一个类只有一个实例并提供一个全局访问点。下次当你发现某个对象整个项目只需要一个的时候别犹豫单例模式安排上。但也要记得模式是工具不是信仰——适合的才是最好的。
返回列表