深入理解JavaScript设计模式之单例模式
目录
- 什么是单例模式
- 为什么需要单例模式
- 常见应用场景包括
- 单例模式实现
- 透明单例模式实现
- 不透明单例模式
- 用代理实现单例模式
- javaScript中的单例模式
- 使用命名空间
- 使用闭包封装私有变量
- 惰性单例
- 通用的惰性单例
- 结语
什么是单例模式
单例模式(Singleton Pattern)
就像‘独生子女’,确保某个类在整个系统中只有一个实例,并提供一个全局访问点。简单来说,一个类一辈子只有一个孩子
。在 JavaScript
编程世界中,例如 Web
应用中的登录窗口,无论你点击多少次登录按钮,只会弹出一次。
为什么需要单例模式
想象一下,你是DR
钻戒的老板。我们推出一款特殊的钻戒——“单例钻戒”。这款钻戒一生只能购买一次,每位顾客终身仅能拥有一枚。这正是单例模式(Singleton Pattern)
的生动写照。
常见应用场景包括
- 浏览器中的任务管理器或标签页管理:保证只有一个全局管理器在运行。
- 通知中心 / 消息中心:统一处理应用内的消息推送和响应。
- 侧边栏 / 弹窗组件:页面中始终只显示一个实例,避免重复创建。
- 全局配置文件管理:读取并维护一份全局配置,供整个应用使用。
- 如
Vue
的Vuex
,React
的Context
或Redux
)和组件生命周期管理机制。
通过单例模式,我们可以有效地控制资源的使用,减少重复对象的创建,提升系统性能与稳定性。
单例模式实现
透明单例模式实现
var Singleton = function (name) {this.name = name;};Singleton.prototype.getName = function () {alert(this.name);};Singleton.getInstance = (function () {var instance = null;return function (name) {if (!instance) {instance = new Singleton(name);}return instance;};})();var a = Singleton.getInstance( 'sven1' ); var b = Singleton.getInstance( 'sven2' ); alert ( a === b ); // true
上面的代码中是一个简单实现单例模式的方式,通过调用Singleton.getInstance()
来获取Singketon
类的唯一实例,实现很简单,但是有一个缺点,降低了类的“透明性”
。
通常情况下,使用一个类的时候,习惯用new ClassName()
的方式来新建一个对象,但是Singleton
类并不是通过new
直接创建实例的,而是通过Singleton.getInstance()
这个静态方法来获取唯一的实力,这就必须要求使用这个类的人必须知道它是一个单例模式,并且要记住不能用new来创建对象,必须使用Singleton.getInstance()
来创建对象,这种写法会让类的使用变得不够直观和不透明,增加了使用上的认知负担,很容易导致吴用。
不透明单例模式
var CreateDiv = (function () {var instance;var CreateDiv = function (html) {if (instance) {return instance;}this.html = html;this.init();return (instance = this);};CreateDiv.prototype.init = function () {var div = document.createElement("div");div.innerHTML = this.html;document.body.appendChild(div);};return CreateDiv;})();var a = new CreateDiv("sven1");var b = new CreateDiv("sven2");console.log("a:", a);console.log("b:", b);console.log(a === b);
在上一步骤中,我们实现了一个不透明的单例模式,即用户必须通过 Singleton.getInstance()
的方式来获取实例,而不是使用常见的 new
方式创建对象。这种方式虽然实现了单例的效果,但不够直观,降低了类的“透明性”
。
为了改进这一点,我们可以尝试实现一个透明
的单例模式。所谓“透明”
,是指用户在使用时可以像使用普通类一样,直接通过 new CreateDiv()
的方式来创建实例,而无需关心这是一个单例类。
我们通过一个自执行的闭包函数来实现这个目标。在这个闭包中,我们将实例变量(如 instance
)封装为“私有变量”
,使其不会被外部轻易访问或修改,并且在整个页面不刷新的情况下,始终只存在一个实例。
例如,我们定义了一个 CreateDiv
类,它在被 new
调用时会检查是否已经存在实例,如果存在,则返回已有的实例,从而保证了全局唯一性。整个过程对使用者是透明的,使用方式和普通类完全一致,这样一来,既保留了单例的核心特性,又提升了使用的直观性和代码的可维护性。
虽然很流畅,很有单例的感觉,但是透明的单例模式还是有缺点的:
- 代码复杂度提高了:为了封装单例逻辑,使用了闭包和自执行函数,让代码变得不容易理解。
- 构造函数职责不单一:
createDiv
构造函数不仅要负责创建对象和初始化,还要控制只能有一个单例的逻辑,违反了"单一职责原则"- 不容易扩散或者修改:如果我们还想把这个类从
“只能创建一个实例”
改为“可以创建多个实例”
,就必须去修改构造函数内部的逻辑,非常不灵活,虽然这个写法让使用更自然(像普通类一样用 new 创建实例),但实现起来更复杂,维护成本也更高
用代理实现单例模式
var CreateDiv = function (html) {this.html = html;this.init();};CreateDiv.prototype.init = function () {var div = document.createElement("div");div.innerHTML = this.html;document.body.appendChild(div);};var ProxySingletonCreateDiv = (function () {var instance;return function (html) {if (!instance) {instance = new CreateDiv(html);}return instance;};})();var a = new ProxySingletonCreateDiv("sven1");var b = new ProxySingletonCreateDiv("sven2");console.log("a", a);console.log("b", b);console.log(a === b);
上面代码对
透明
单例模式的代理模式进行了优化,原来的CreateDiv
类在构造函数中直接包含了控制单例的逻辑导致职责不清楚,不容易扩展,现在通过引入一个代理类ProxySingletonCreateDiv
,将管理单例的逻辑从CreateDiv
中抽离出去,让它回归为一个存粹用于创建div
元素的普通类,具体来说,CreateDiv
只负责创建对象和初始化操作,比如生成div
并插入页面,单例的逻辑则有代理函数ProxySingletonCreateDiv
来处理,这个代理函数是一个闭包,内部维护了一个唯一的实例,当第一次调用的时候会创建CreativeDiv
实力,之后直接返回该实例,这种方法是缓存代理的一种典型应用,让CreateDiv
更加的灵活,通用,同时又不丢失单例的特性。
javaScript中的单例模式
上面的集中单例模式实现方式偏向于传统的面向对象的写法,即通过"类"来创建唯一的对象,但是在javaScript
中,这种做法也不完全适用,javaScript
在es5
中是一门无类(class-free)
语言,创建对象非常简单,如果只需要一个唯一的对象,没必要先定义一个类再来创建实例,要知道单例模式的核心即可:“确保只有一个实例,并提供全局访问”
,最然全局变量具备唯一
和全局访问
的特性,但是它并不是真正的单例模式,而且存在命名冲突,全局作用域污染,在大型的项目中使用全局作用域尤其危险,为了减少全局变量污染带来的问题,可以采取使用命名空间和使用闭包封装私有变量解决。
使用命名空间
适当的使用命名空间,并不会杜绝全局变量,但是可以减少全局变量的数量,最简单的方法就是对象的字面量的方式解决:
var namespace1 = {a: function () {alert(1);},b: function () {alert(2);},};
把 a
和 b
都定义为 namespace1
的属性,这样可以减少变量和全局作用域打交道的机会。
使用闭包封装私有变量
var user = (function () {var __name = "sven",__age = 29;return {getUserInfo: function () {return __name + "-" + __age;},};})();
将一些变量封装在闭包的内部,只暴露一些接口跟外界通信,用下划线来约定私用变量__name
和__age
,它们被封装在闭包产生的作用域中,外部变量访问不到这两个变量,这就进行了全局的隔绝污染。
惰性单例
前面了解了单例模式的一些实现方法,接下来学习一下单例模式比较重要的一个知识"惰性单例【Lazy Singleton
】",惰性单例指的是需要的时候才创建实例,而不是在页面加载的时候就提前创建,这种方式更节省资源,实际开发中,以弹窗为例,如果一开始就创建弹窗元素,用户可能根本不需要弹出窗口,这样回造成资源浪费,如果每次点击按钮都创建一个新的弹窗,虽然实现了按需展示,但却失去了单例的唯一性,正确的做法是首次点击创建弹窗,再次点击直接使用已经创建好的弹窗实例,这样就实现了惰性单例,即实现了只创建一次,又坐到了用的时候才创建,兼顾了功能与性能。
<html><head><style>.modal {position: fixed;width: 200px;height: 200px;line-height: 200px;text-align: center;left: 50%;top: 50%;transform: translate(-50%, -50%);background: goldenrod;}</style></head><body><button id="open">打开</button><button id="close">关闭</button><script>const Moda = (function () {let instance = null;return function () {if (!instance) {instance = document.createElement("div");instance.innerHTML = "模态框";instance.className = "modal";instance.style.display = "none";document.body.appendChild(instance);}return instance;};})();document.getElementById("open").onclick = function () {// 创建一个模态框const modal = Moda();modal.style.display = "block";};document.getElementById("close").onclick = function () {// 删除模态框const modal = Moda();modal.style.display = "none";};</script></body>
</html>
通用的惰性单例
我们知道了什么是惰性单例和如何实现惰性单例,但是不难发现,普通的惰性单例还是违反了单一职责
原则,创建对象和管理单例的逻辑都放在了Moda
内部,如果我们下次需要创建页面唯一的iframe
或者script
或者更多类型的标签,就必须如法炮制创建更多的Moda
函数,几乎照抄一遍,针对这种方法需要将不变的部分隔离出来,创建div
,iframe
,img
逻辑其实可以完全抽象出来【使用一个变量标志是否创建过,如果创建过则下次直接返回这个已经常见好的对象】。
return function () {if (!instance) {instance = document.createElement("div");... }return instance;};
接下来就把如何管理单例的逻辑抽离出来,这些逻辑封装在getSingle函数内部,创建对象的方法fn被当成参数动态传入getSingle函数。
var getSingle = function( fn ){ var result; return function(){ return result || ( result = fn .apply(this, arguments ) ); }
};
接下来将用于创建登录浮窗的方法用参数 fn
的形式传入 getSingle
,我们不仅可以传入Mode
,还能传入 ModeScript
、ModeIframe
、ModeXhr
等。之后再让getSingle
返回一个新的函数,并且用一个变量 result
来保存fn
的计算结果。result
变量因为身在闭包中,它永远不会被销毁。在将来的请求,如果 result 已经被赋值,那么它将返回这个值。
<html><head><style>.modal {position: fixed;width: 200px;height: 200px;line-height: 200px;text-align: center;left: 50%;top: 50%;transform: translate(-50%, -50%);background: goldenrod;}</style></head><body><button id="open">打开</button><button id="close">关闭</button><script>let getSingle = function (fn) {let result;return function () {return result || (result = fn.apply(this, arguments));};};let ModelDiv = function () {let div = document.createElement("div");div.innerHTML = "模态框";div.className = "modal";div.style.display = "none";document.body.appendChild(div);return div;};let ModelIframe = function () {let iframe = document.createElement("iframe");iframe.style.display = "none";document.body.appendChild(iframe);return iframe;};var createSingleLoginLayer = getSingle(ModelDiv);var createSingleLoginLayer2 = getSingle(ModelIframe);document.getElementById("open").onclick = function () {// 创建一个模态框const modal = createSingleLoginLayer();const ModelIframe1 = createSingleLoginLayer2();modal.style.display = "block";ModelIframe1.style.display = "block";};document.getElementById("close").onclick = function () {// 删除模态框const modal = createSingleLoginLayer();const ModelIframe2 = createSingleLoginLayer2();modal.style.display = "none";ModelIframe2.style.display = "none";};</script></body>
</html>
结语
设计模式不是“炫技”,而是"沉淀",希望我们通过阅读和学习《JavaScript设计模式》和实践中,在显示业务需求开发中写出更具有可维护性,可扩展性的代码。
致敬—— 《JavaScript设计模式》· 曾探
相关文章:

深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...

STM32F4基本定时器使用和原理详解
STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...

【机器视觉】单目测距——运动结构恢复
ps:图是随便找的,为了凑个封面 前言 在前面对光流法进行进一步改进,希望将2D光流推广至3D场景流时,发现2D转3D过程中存在尺度歧义问题,需要补全摄像头拍摄图像中缺失的深度信息,否则解空间不收敛…...

蓝牙 BLE 扫描面试题大全(2):进阶面试题与实战演练
前文覆盖了 BLE 扫描的基础概念与经典问题蓝牙 BLE 扫描面试题大全(1):从基础到实战的深度解析-CSDN博客,但实际面试中,企业更关注候选人对复杂场景的应对能力(如多设备并发扫描、低功耗与高发现率的平衡)和前沿技术的…...
【磁盘】每天掌握一个Linux命令 - iostat
目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat(I/O Statistics)是Linux系统下用于监视系统输入输出设备和CPU使…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院挂号小程序
一、开发准备 环境搭建: 安装DevEco Studio 3.0或更高版本配置HarmonyOS SDK申请开发者账号 项目创建: File > New > Create Project > Application (选择"Empty Ability") 二、核心功能实现 1. 医院科室展示 /…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
基于Uniapp开发HarmonyOS 5.0旅游应用技术实践
一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架,支持"一次开发,多端部署",可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务,为旅游应用带来…...
服务器硬防的应用场景都有哪些?
服务器硬防是指一种通过硬件设备层面的安全措施来防御服务器系统受到网络攻击的方式,避免服务器受到各种恶意攻击和网络威胁,那么,服务器硬防通常都会应用在哪些场景当中呢? 硬防服务器中一般会配备入侵检测系统和预防系统&#x…...
django filter 统计数量 按属性去重
在Django中,如果你想要根据某个属性对查询集进行去重并统计数量,你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求: 方法1:使用annotate()和Count 假设你有一个模型Item,并且你想…...
Auto-Coder使用GPT-4o完成:在用TabPFN这个模型构建一个预测未来3天涨跌的分类任务
通过akshare库,获取股票数据,并生成TabPFN这个模型 可以识别、处理的格式,写一个完整的预处理示例,并构建一个预测未来 3 天股价涨跌的分类任务 用TabPFN这个模型构建一个预测未来 3 天股价涨跌的分类任务,进行预测并输…...

dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...

抖音增长新引擎:品融电商,一站式全案代运营领跑者
抖音增长新引擎:品融电商,一站式全案代运营领跑者 在抖音这个日活超7亿的流量汪洋中,品牌如何破浪前行?自建团队成本高、效果难控;碎片化运营又难成合力——这正是许多企业面临的增长困局。品融电商以「抖音全案代运营…...
电脑插入多块移动硬盘后经常出现卡顿和蓝屏
当电脑在插入多块移动硬盘后频繁出现卡顿和蓝屏问题时,可能涉及硬件资源冲突、驱动兼容性、供电不足或系统设置等多方面原因。以下是逐步排查和解决方案: 1. 检查电源供电问题 问题原因:多块移动硬盘同时运行可能导致USB接口供电不足&#x…...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...

2.Vue编写一个app
1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...
大语言模型如何处理长文本?常用文本分割技术详解
为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...

全球首个30米分辨率湿地数据集(2000—2022)
数据简介 今天我们分享的数据是全球30米分辨率湿地数据集,包含8种湿地亚类,该数据以0.5X0.5的瓦片存储,我们整理了所有属于中国的瓦片名称与其对应省份,方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...

定时器任务——若依源码分析
分析util包下面的工具类schedule utils: ScheduleUtils 是若依中用于与 Quartz 框架交互的工具类,封装了定时任务的 创建、更新、暂停、删除等核心逻辑。 createScheduleJob createScheduleJob 用于将任务注册到 Quartz,先构建任务的 JobD…...

STM32标准库-DMA直接存储器存取
文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA(Direct Memory Access)直接存储器存取 DMA可以提供外设…...

376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》
在注意力分散、内容高度同质化的时代,情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现,消费者对内容的“有感”程度,正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中࿰…...
在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module
1、为什么要修改 CONNECT 报文? 多租户隔离:自动为接入设备追加租户前缀,后端按 ClientID 拆分队列。零代码鉴权:将入站用户名替换为 OAuth Access-Token,后端 Broker 统一校验。灰度发布:根据 IP/地理位写…...

学校招生小程序源码介绍
基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码,专为学校招生场景量身打造,功能实用且操作便捷。 从技术架构来看,ThinkPHP提供稳定可靠的后台服务,FastAdmin加速开发流程,UniApp则保障小程序在多端有良好的兼…...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...
c++ 面试题(1)-----深度优先搜索(DFS)实现
操作系统:ubuntu22.04 IDE:Visual Studio Code 编程语言:C11 题目描述 地上有一个 m 行 n 列的方格,从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子,但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

家政维修平台实战20:权限设计
目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系,主要是分成几个表,用户表我们是记录用户的基础信息,包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题,不同的角色…...

最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...

基于当前项目通过npm包形式暴露公共组件
1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹,并新增内容 3.创建package文件夹...