设计模式之——单例模式

news2024/11/24 17:50:09

✍🏼作者:周棋洛,计算机学生
♉星座:金牛座
🏠主页:点击学习更多
🌐关键:JavaScript 单例 设计模式
单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

文章目录

  • 一、引言 🎉
  • 二、实现单例 ✌️
  • 三、透明的单例 🪟
  • 四、代理实现单例 ⛏️
  • 五、JavaScript中的单例 📦
  • 六、惰性单例 🍕
  • 七、通用惰性单例 🥛
  • 八、总结 📄

在这里插入图片描述

一、引言 🎉

在日常生活中,经常会遇到一些独一无二、独特而又特殊的事物。有时候,这些事物的独特性是为了保护它的重要性,有时候则是为了提高效率和资源利用。

而在软件开发领域,单例设计模式就是一种让对象独一无二的方式。

单例模式是一种常用的模式,属于创建型设计模式。有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的 window 对象等。

在 JavaScript 开发中,单例模式的用途同样非常广泛。比如页面中的登录浮窗是唯一的,无论单击多少次按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。

在这里插入图片描述

二、实现单例 ✌️

想要实现一个标准的单例模式并不复杂,用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建好的对象即可。用代码模拟一下:

let Singleton = function (name) {
    this.name = name;
}
Singleton.getInstance = (function () {
	// 闭包,实例标记
    let instance = null;
    return function (name) {
        if (!instance) {
            instance = new Singleton(name);
        }
        return instance;
    }
})();

先定义一个构造函数,然后对外暴露了一个方法用来获取对象实例,这个方法中使用自执行函数返回一个闭包函数,借助闭包保留了 instance 局部变量的状态。

判断实例是否存在,如果不存在创建并返回,否则直接返回已有实例。确保了只创建一次。

// 测试
let a = Singleton.getInstance("王子周棋洛");
let b = Singleton.getInstance("王子");

console.log(a === b); // true

我们已经实现了一个简单的单例,但是它并不完美,看下面代码:

 let n1 =  new Singleton("周棋洛");
 let n2 =  new Singleton("周棋洛王子");

 console.log(n1 === n2); // false

当用户使用new关键字创建对象时,我们的单例就控制不住了。下面,我们针对这种情况对代码做一些处理,实现一个透明的单例模式。

在这里插入图片描述

三、透明的单例 🪟

透明的单例模式,即用户从这个类创建对象时,可以像其他任意普通类一样。而不是只能通过我们规定的对外暴露的方法创建。

下面我们将写一个创建元素的方法,保证页面只创建一次,单实例。

 let createP = (function () {
     let instance;
     // 重写构造函数
     let createP = function (text) {
         if (instance) {
             return instance;
         }
         this.text = text;
         this.init();
         return instance = this;
     }

     createP.prototype.init = function () {
         let p = document.createElement('p');
         p.innerHTML = this.text;
         document.body.appendChild(p);

     }

     // 返回新的构造函数
     return createP;
 })()

 // 测试
 let a = new createP('你好');
 let b = new createP('世界');

 console.log(a === b); // true

现在实现了一个透明的单例类,但它同样有一些缺点。

为了把 instance 状态保存起来,使用了自执行的匿名函数和闭包,并让这个匿名函数返回真正的构造方法,这增加了一些程序的可读性。

// 重写构造函数,闭包
let createP = function (text) {
    if (instance) {
        return instance;
    }
    this.text = text;
    this.init();
    return instance = this;
}

其次,观察重写的构造函数,他做了两件事,一是保证单例,二是执行初始化init方法创建P标签。根据“单一职责原则” ,这里的代码写的并不漂亮,假如如果我们有了新需求,创建100个P标签,怎么办,做法是新建一个方法把这个方法的单实例职责去掉,这样的修改会给我们带来不必要的烦恼。

“单一职责原则”:简单来说,就是一个类或者模块应该只负责一项职责,不要承担过多的职责,以达到代码更加清晰、简洁、易于维护的目的。

如果一个类或者模块承担的职责过多,一旦需要修改其中一部分功能时可能会影响其他功能,增加了耦合度,也会导致代码的复杂性增加,难以理解和维护。

下面我们将引入代理类,来解决上面职责过多的问题。

在这里插入图片描述

四、代理实现单例 ⛏️

根据上面提到的单一职责,把负责单例的代码移除,使它变成一个普通的创建类。

 // 单一职责,负责创建dom
 let createP = function (text) {
     this.text = text;
     this.init();
 }

 createP.prototype.init = function () {
     let p = document.createElement('p');
     p.innerHTML = this.text;
     document.body.appendChild(p);
 }

 // 引入代理类,闭包,单实例
 let ProxySingletonCreateP = (function () {
     let instance;
     return function (text) {
         if (!instance) {
             instance = new createP(text);
         }
         return instance;
     }
 })();

 // 测试
 let a = new ProxySingletonCreateP('你好');
 let b = new ProxySingletonCreateP('世界');

 console.log(a === b); // true

与之前不同的是,通过引入代理类,把负责管理单例的逻辑转到了代理类 ProxySingletonCreateP 中。

这样一来,createP 就变成了一个普通的单一职责类。

在这里插入图片描述

五、JavaScript中的单例 📦

在Java中,如果需要对象,首先要定义一个类,对象总是从类创建出来的。但是在 JavaScript 这个无类的语言中,创建对象十分简单。

JavaScript 对象的创建底层包含了四个步骤:创建新的空对象、设置对象原型、将 proto 属性指向该原型对象、执行构造函数或者直接返回字面量对象。

这些步骤都是为了将属性和方法添加到新对象中,并与原型对象建立联系。

在 JavaScript 中,全局变量是单例模式的一种体现。单例模式指的是只创建一个实例对象,并提供一个访问它的全局访问点。全局变量在 JavaScript 中也是只有一个实例,可以通过任何地方访问它,因此也符合单例模式的定义。

let a = {}

但是,需要注意的是,在 JavaScript 中过多地使用全局变量可能会导致命名空间污染和代码维护的困难。

因此,最好尽量避免使用全局变量,即使需要,也要把它的污染降到最低。

例如可以使用命名空间:

let namespace1 = {
	a:10,
	b:20
};

或者借助闭包封装私有变量等等:

 let user = (function () {
     let _name = '王子';
     return {
         getUserInfo: function () {
             return _name;
         }
     }
 })();

 console.log(user.getUserInfo()); // 王子

使用下划线约定私有变量,变量被封装在闭包产生的作用域中,外部访问不到这些变量,这就避免了对全局的命名污染。

在这里插入图片描述

六、惰性单例 🍕

惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点,这种技术在开发中非常有用,实用程度可能超出了我们的想象。

现在有这样一个场景,我的网站接入了用户登录功能,登录悬浮窗只能存在一个,单实例。

在这里插入图片描述

我们可以在网页加载时,就创建出这个悬浮框,然后最开始的时候通过css将它隐藏,当点击登录按钮时,通过css将它显示出来,这样就确保了登录悬浮框的单实例。

但这种方式有一个问题,如果进入网页只是看看,根本不需要进行登录操作,因为登录悬浮框总是在网站加载就被创建好,那将白白浪费一些 DOM 节点。

简单改一下逻辑,当点击登录按钮时,再惰性创建登录悬浮窗,代码如下:

<body>
    <button>登录</button>

    <script>
        function createLoginBox() {
            let div = document.createElement("div");
            div.style.width = "300px";
            div.style.height = "200px";
            div.style.marginTop = "15px";
            div.style.background = "#bbb";
            div.innerHTML = "这是一个登录框";
            document.body.appendChild(div);
        }

        document.querySelector("button").addEventListener("click", () => {
            createLoginBox();
        })
    </script>
</body>

虽然实现了惰性创建的需求,但是失去了单例的效果,就像这样:

在这里插入图片描述

我们可以通过一个变量判断当前是否已经创建过登录悬浮框,如果有就不再创建了,否则创建它,这里借助闭包实现,修改后的代码如下:

<body>
    <button>登录</button>

    <script>
        let createLoginBox = function () {
            let box;
            return function () {
                if (!box) {
                    let div = document.createElement("div");
                    div.style.width = "300px";
                    div.style.height = "200px";
                    div.style.marginTop = "15px";
                    div.style.background = "#bbb";
                    div.innerHTML = "这是一个登录框";
                    document.body.appendChild(div);
                    box = div;
                }
                return box;
            }
        }();

        document.querySelector("button").addEventListener("click", () => {
            createLoginBox();
        })
    </script>
</body>

借助了闭包,将变量box封装在闭包产生的作用域中,通过判断变量box来决定是否创建登录悬浮框,这样就解决问题了。

在这里插入图片描述
现在多次点击登录,页面也只会有一个登录悬浮框。

在这里插入图片描述

七、通用惰性单例 🥛

上面我们完成了惰性单例的功能,但是代码违反了单一职责原则,这里就不再过多解释单一职责了,前文已经说过了,代码将创建dom逻辑和单例判断都放到了createLoginBox内部实现。

如果有个新需求,创建一个唯一的搜索框,就要把上面的代码复制粘贴,修改一下创建逻辑,这不好吧!

所以我们需要分析变与不变,在上面的需求里,变化的是你要创建什么,不变的是单例的逻辑,所以我们可以把实现单例的逻辑抽出来,因为它始终不变,代码如下:

 let getSingleton = function (fn) {
     let instance;
     return function () {
         // 如果实例存在,直接返回,否则调用函数并给instanc赋值
         return instance || (instance = fn.apply(this, arguments));
     }
 }

this 在 apply() 中指向的是当前上下文中的 this 对象,也就是 getSingleton 函数本身。

因此,这里使用 apply() 这种方式可以确保 fn 函数中的 this 指向的是当前上下文中的 this 对象。

在第二次及以后的调用中,由于 instance 已经被赋值了,所以直接返回 instance 即可,避免了重复创建实例,提高了性能。

现在我想创建单一实例的登录悬浮框,注册悬浮框,代码这样写就可以了:

<body>
    <button>登录</button>
    <button>注册</button>

    <script>
        let createLoginBox = function () {
            let div = document.createElement("div");
            div.style.width = "300px";
            div.style.height = "200px";
            div.style.marginTop = "15px";
            div.style.background = "#bbb";
            div.innerHTML = "这是一个登录框";
            document.body.appendChild(div);
            return div;
        };

        let createRegisterBox = function () {
            let div = document.createElement("div");
            div.style.width = "300px";
            div.style.height = "200px";
            div.style.marginTop = "15px";
            div.style.background = "#bbb";
            div.innerHTML = "这是一个注册框";
            document.body.appendChild(div);
            return div;
        };

        let getSingleton = function (fn) {
            let instance;
            return function () {
                // 如果实例存在,直接返回,否则调用函数并给instanc赋值
                return instance || (instance = fn.apply(this, arguments));
            }
        }


        let createSingletonLoginBox = getSingleton(createLoginBox);
        let createSingletonRegisterBox = getSingleton(createRegisterBox);

        document.querySelectorAll("button")[0].addEventListener("click", () => {
            createSingletonLoginBox();
        })
        document.querySelectorAll("button")[1].addEventListener("click", () => {
            createSingletonRegisterBox();
        })
    </script>
</body>

无论点击多少次,他们都只会被创建一次。

在这里插入图片描述
在这个案例中,我们把创建实例对象的职责和管理单例的职责分别放置在两个方法里,这两个方法可以独立变化而互不影响,当它们连接在一起的时候,就完成了创建唯一实例对象的功能,是不是挺优雅的。

这种单例模式的用途远不止创建对象,比如要给这个列表绑定 click 事件,测试代码如下:

<body>
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
    </ul>

    <script>
        let getSingleton = function (fn) {
            let instance;
            return function () {
                // 如果实例存在,直接返回,否则调用函数并给instanc赋值
                return instance || (instance = fn.apply(this, arguments));
            }
        }

        let bindEvent = function () {
            console.log("===开始绑定===");
            document.querySelectorAll("li").forEach(li => {
                li.addEventListener("click", () => {
                    console.log("点击了")
                })
            })
            return true;
        }

        let bindEventSingle = getSingleton(() => {
            console.log("===单例开始绑定===");
            document.querySelectorAll("li").forEach(li => {
                li.addEventListener("click", () => {
                    console.log("单例点击了")
                })
            })
            return true;
        })

        bindEvent();
        bindEvent();
        bindEvent();

        bindEventSingle();
        bindEventSingle();
        bindEventSingle();
    </script>
</body>

在这里插入图片描述
你学到了吗?少年

在这里插入图片描述

八、总结 📄

单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

因为语言的差异性,在 JavaScript 中有更适合它的方法创建单例。本文还用到了代理模式和单一职责原则。

在 getSinge 函数中,实际上用到了闭包和高阶函数的概念。单例模式是一种简单但非常实用的模式,特别是惰性单例技术,使用非常广泛,在合适的时候才创建对象,并且只创建唯一的一个。

更酷的是,创建对象的职责和管理单例的职责被拆分在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。你学到了吗,少年?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/663326.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【操作系统】程序运行环境

目录 1.处理器运行模式 1.1特权指令 1.2非特权指令 2.中断和异常的概念 2.1中断和异常的定义 2.1.1外中断 2.1.2内中断&#xff08;异常&#xff09; 2.2中断和异常的处理过程 3.系统调用 1.处理器运行模式 计算机系统中&#xff0c;通常 CPU 执行两种不同性质的程序:一…

【Android开发基础】手机传感器信息的获取

文章目录 一、引言二、了解1、概述2、关键 三、设计1、UI设计&#xff08;1&#xff09;主界面&#xff08;2&#xff09;适配器item 2、编码&#xff08;1&#xff09;获取数据&#xff08;传感器信息&#xff09;&#xff08;2&#xff09;渲染数据&#xff08;初始化适配器&…

SpringBoot不在使用@Validated 做参数校验但是不想在Controller层怎么办?

目录 场景再现&#xff1a; 怎么做&#xff1f; 遇到了什么问题&#xff1f; 怎么实现&#xff1f; 场景再现&#xff1a; 某API接口接受加密的json字符串&#xff0c;接受字符串之后先进行解密处理&#xff0c;解密完成之后还要进行参数校验处理&#xff0c;如果参数不合规…

华为HCIA备考(数通) 易错题整理 PART1

1.IEEE802.1Q定义的 VLAN 帧格式中VLAN ID总共有多少bit 答&#xff1a;12 2.NAPT允许多个私有IP地址通过不同的端口号映射到同一个公有IP地址上&#xff0c;且不需要做任何关于端口号的配置。 3.IEEE802.1Q定义的VLAN帧总长度为多少字节&#xff1f; 答&#xff1a;4 4.关于…

2023亚马逊云科技中国峰会:强化学习探索—— Amazon DeepRacer

1️⃣前言 Amazon DeepRacer 是一个综合性的学习系统&#xff0c;可供各个水平的用户用来学习和探索强化学习以及试验和构建自动驾驶应用程序。 2️⃣Amazon DeepRacer 介绍 DeepRacer是一款小型自主驾驶车辆&#xff0c;它结合了深度学习、强化学习和云计算等技术&#xff0c…

Karl Guttag评Vision Pro(二):这些硬件问题不容忽视

上周&#xff0c;AR/VR光学专家Karl Guttag从价格、VST透视、应用、交互等方面&#xff0c;将Vision Pro与Quest Pro进行对比&#xff0c;发现Vision Pro在设计上做出更多正确决策。尽管如此&#xff0c;Guttag认为该头显本身在硬件部分&#xff0c;依然存在一些不易发现的问题…

C语言结构体计算大小结构体的对齐数,修改默认对齐数。

结构体的对齐规则 第一个成员在与结构体变量偏移量为0的地址处。剩余成员变量要对齐到对齐数的整数倍的地址处。 对齐数&#xff1a;编译器默认对齐数与该成员大小的较小值vs中默认对齐数为8没有默认对齐数&#xff0c;那么对齐数就是其本身成员大小 结构体总大小为最大对齐数…

2个小工具让你轻松解决大数据/数据库测试

目录 前言&#xff1a; 研究背景 工具 支持功能 支持功能 使用方法 总结 前言&#xff1a; 做大数据/数据库测试时&#xff0c;通常需要编写复杂的测试用例代码或手动操作来测试系统的正确性和稳定性。但是&#xff0c;有些小工具可以轻松解决这个问题&#xff0c;提高测试效率…

python自动化测试- 自动化框架及工具

1 概述 手续的关于测试的方法论&#xff0c;都是建立在之前的文章里面提到的观点&#xff1a; 功能测试不建议做自动化接口测试性价比最高接口测试可以做自动化 后面所谈到的 测试自动化 也将围绕着 接口自动化 来介绍。 如果你想学习自动化测试&#xff0c;我这边给你推…

智慧绿色档案馆之八防一体化解决系统方案

主要涉及系统&#xff1a; 智慧档案馆温湿度监控系统 智慧档案馆净化系统 智慧档案馆防火监控系统 智慧档案馆防盗监控系统 智慧档案馆漏水监控系统 智慧档案馆空气质量监控系统 智慧档案馆自动化恒温恒净化系统 智慧档案馆大数据云平台建设系统 &#xff08;一&#xff09;技…

C语言实现青蛙跳台阶问题【图解】

目录 问题分析方法1&#xff1a;找规律问题分析方法2&#xff1a;递归两种方法的代码 题目 一只青蛙一次可以跳上1级台阶&#xff0c;也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法&#xff1f; 问题分析方法1&#xff1a;找规律 当n1时&#xff0c;显然…

vue+Nodejs+Koa搭建前后端系统(六)-- 用户登录

前言 采用vue3&#xff0c;vue-router版本为4.x前端构建工具采用viteIDE采用VSCODE&#xff0c;安装了MYSQL客户端插件 前端编写 安装并使用 vue-router 如果有vue-router&#xff0c;就略过这一小节。 vue-router完整教程&#xff1a;点这里>> 第一步&#xff1a;n…

MySQL ⽀持哪些存储引擎?默认使⽤哪个?MyISAM 和 InnoDB 引擎有什么区别,如何选择?

&#x1f4a7; M y S Q L ⽀持哪些存储引擎&#xff1f;默认使⽤哪个&#xff1f; M y I S A M 和 I n n o D B 引擎有什么区别&#xff0c;如何选择&#xff1f; \color{#FF1493}{MySQL ⽀持哪些存储引擎&#xff1f;默认使⽤哪个&#xff1f;MyISAM 和 InnoDB 引擎有什么区别…

centos tomcat

利用samba共享将所用的的组件复制到/usr/java/中 并给/usr/java设置777权限 Samba配置如下 重启smb服务 进入目录开始安装jdk 在当前目录解压jdk 配置环境变量 使配置生效 查看jdk版本 创建tomcat目录 解压tomcat的安装包到当前目录 进入tomcat配置目录 启动tomcat 先关闭在启…

【微服务】微服务架构设计

文章目录 背景一、流量入口Nginx二、网关三、业务组件四、服务注册中心五、缓存和分布式锁六、数据持久层七、结构型数据存储八、消息中间件九、日志收集十、任务调度中心十一、分布式对象存储 背景 当前&#xff0c;微服务架构在很多公司都已经落地实施了&#xff0c;下面用一…

01_Linux系统安装及使用

一、安装虚拟机软件 VMware16pro 安装链接&#xff1a;https://note.youdao.com/ynoteshare/index.html?id5fc5ad640596a0fbb41a21413ada4dad&typenote&_time1687172973066 二、安装Ubuntu 64 位 Linux系统 安装链接&#xff1a;https://note.youdao.com/ynoteshar…

【动态规划】简单多状态dp问题(1)打家劫舍问题

打家劫舍问题 文章目录 【动态规划】简单多状态dp问题&#xff08;1&#xff09;打家劫舍问题1. 按摩师&#xff08;打家劫舍Ⅰ&#xff09;1.1 题目解析1.2 算法原理1.2.1 状态表示1.2.2 状态转移方程1.2.3 初始化1.2.4 填表顺序1.2.5 返回值 1.3 编写代码 2. 打家劫舍Ⅱ2.1 题…

seaborn笔记:heatmap

绘制热力图 1 基本使用方法 seaborn.heatmap(data, *, vminNone, vmaxNone, cmapNone, centerNone, robustFalse, annotNone, fmt.2g, annot_kwsNone, linewidths0, linecolorwhite, cbarTrue, cbar_kwsNone, cbar_axNone, squareFalse, xticklabelsauto, yticklabelsauto, m…

C++ 教程(16)——字符串

C 字符串 C 提供了以下两种类型的字符串表示形式&#xff1a; C 风格字符串C 引入的 string 类类型 C 风格字符串 C 风格的字符串起源于 C 语言&#xff0c;并在 C 中继续得到支持。字符串实际上是使用 null 字符 \0 终止的一维字符数组。因此&#xff0c;一个以 null 结尾…

Mysql的学习笔记

目录 1、rc 级别的 mvcc 和 rr 级别的mvcc 有啥区别&#xff1f; 2、Innodb与MyIsam的文件结构&#xff1f; 3、Innodb 与MyIsam的简单对比&#xff1f; 4、innodb&#xff0c;Alter table 改字段类型&#xff0c;底层会经历什么过程&#xff1f; 5、Alter table 改字段类型…