你能看懂的 JavaScript 设计模式!(2) 单例模式,耍!

大家好,这里是 Murlin,一个专注前端开发的老程序员,分享的每篇文章都是为了帮助你更好的理解前端知识。

我们的口号是:没有..哦不对,是拯救头发!

我们终于开始设计模式的学习啦,其实在之前应该和大家再多聊一下关于 封装、多态、闭包、高阶函数相关的知识,因为这些都是我们学习设计模式的基础,但是担心大家看着看着不耐烦,不管了,设计模式先耍起来,其他的后面再搞~

一、什么是单例模式?

很久很久以前,天上有九个太阳,不分白天黑夜就是哐哐一顿发光发热,不知道太阳天天 007 的上班受不受得了,老百姓是真的遭不住了,于是一个叫后羿的英雄出来拿了九杀,赢得了比赛。

归根结底,是因为天上有一个太阳就足够了,同样属性和作用的太阳不仅不会产生更好的效果,有时候还会适得其反。

项目开发也是如此,同样功能及作用的对象,有一个就够了,再多只会导致维护起来比较混乱,还会增加性能成本。

比如我们在一些PC网站中,当我们没有登录的时候,无论点击「个人中心」还是「发布内容」等按钮,都会弹出一个登录弹框,而这个弹窗就需要是唯一的,无论点击哪个按钮,点击多少次,都只会创建一个弹窗,后续的点击就是控制他的显示隐藏即可,这样不仅代码更清晰,而且也避免了不断创建节点和销毁节点的性能损耗。

上面说的针对「登录弹框」的实现方案,采用的其实就是单例模式的思想。

那么我们为单例模式下一个书面化语言的定义:

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

说人话就是:

对于项目中全局性的一些效果或者功能,只搞一个出来,并且不管用模块化还是什么方法,搞到全局上,大家一起用它就行。

二、JavaScript中的单例

有同学可能会想,单例模式嘛,很简单,一个类仅有一个实例的话,我还搞什么类,整个Object对象得了,反正JS中一切皆对象,来嘛,搞嘛。

比如,我们在项目中常会单独设计一个模块来做一些数字计算、字符串操作的通用方法,那我们搞一个对象来就好了呗:

js 复制代码
const math = {
  add: function (a, b) {
    return a + b;
  },
  sub: function (a, b) {
    return a - b;
  }
}

普普通通,看起来用起来没啥问题,其实再好好研究下,还是存在一些问题的,要全局的用,那就得搞到全局空间上,就很可能会出现变量命名冲突的问题,毕竟一个项目开发的同事比较多,咱管得了自己,可不好管别人呀。

而且某一天我们接收其他项目的时候,想把这个工具搬过去是不是也得考虑着点儿变量名冲突的问题。

其实这个问题也好解决,搞命名空间、搞闭包,都是可以实现的,这个方法咱们有时间再分享。

而且的而且,现在大家都是利用webpack、vite等工具实现模块化开发了,单独模块在打包后其实是会形成独立命名空间的,所以其实也不用太多考虑这个问题,一个模块里做到不重名、不冲突还是很容易做到的嘛。

说这么多,总结一下就是:

JS因为自己独特的灵活性,实现单例模式其实很简单,只要注意好命名冲突这个大问题基本就可以了。

那这篇文章就可以完结撒花了嘛?耐乌!当然不是,我们学习设计模式当然要结合时代的变化,在没有模块化前我们想搞单例,采用上面的办法其实是很不容易的,而且这种方式看起来也有点脆弱,总是感觉背后有很多大坑在等着。

那么接下来咱们就来研究下正常套路的单例,去感受下前辈大拿是怎么耍起来的。

三、单例模式的基本实现

常规的单例模式的实现也比较简单,我们研究这个的时候,不要把重点放在实例功能的实现上,而是去关注如何保证每次使用的时候不会创建新的实例

下面我们小撸一点点代码出来:

js 复制代码
const Singleton = function (name) {
  this.name = name;
  // 当前类的实例
  this.instance = null;
}
// 类的功能方法, 做示意用, 在这里没有实际意义
Singleton.prototype.getName = function () {
  console.log(this.name);
}
// 获取类的实例的方法
Singleton.getInstance = function (name) {
  // 如果实例不存在, 则创建实例 否则则直接返回实例
  if (!this.instance) {
    this.instance = new Singleton(name);
  }
  return this.instance;
}

const a = Singleton.getInstance('murlin1');
const b = Singleton.getInstance('murlin2');

a.getName(); // murlin1
b.getName(); // murlin1 实质上只有一个实例, 所以这里的name是一样的
console.log(a === b); // true

这样,一个最最简单的单例模式的实现就搞完啦,大家可以看到上面代码中a和b实例上使用的都是同一个实例,这不就拿捏了么,泼饭!

但是总感觉有点问题,比如,如果别的同事不知道Singleton是一个单例的类,他去new Singleton()使用了怎么办?而且确实也不怪人家这么用,毕竟10个实例9个半都是new出来的,getInstance是不是有点太反常识了。

这种情况有个书面语哈,说是这样的类呢不太透明 ,这样搞增加了类的不透明性,无所D谓,优化它不就得了,来,继续!

四、透明的单例模式

我们接下来实现一个透明的、合群的单例,让创造出来的类的使用方式和其他的类使用方式一样,不搞特殊化。

我们换一个案例,去实现一个叫CreateDiv的类,这个类的作用就是在页面中创建一个唯一的div。

小代码搞起来:

js 复制代码
// 利用闭包的特性 创建instance私有变量来控制唯一实例
const CreateDiv = (function () {
  // 闭包变量, 存放唯一实例
  let instance = null;
  // 真正的类构造函数
  const CreateDiv = function (html) {
    // 如果实例已经存在, 则直接返回实例
    if (instance) return instance;
    // 如果实例不存在, 则创建实例
    this.html = html;
    this.init();
    return instance = this;
  }
  // 创建唯一的div的方法, 只有实例不存在的时候才会进行创建
  CreateDiv.prototype.init = function () {
    const div = document.createElement('div');
    div.innerHTML = this.html;
    document.body.appendChild(div);
  }
})()

const a = new CreateDiv('murlin1');
const b = new CreateDiv('murlin2');

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

大家可以看到,这个时候CreateDiv已经变成了一个合群的类了,他的使用方式和其他类没有什么区别,而且可以做到每次new得到的都是唯一的实例。

有同学看到闭包马上想到内存泄露,继而想到性能差,其实吧,这事儿得这么想,可乐虽然含糖量高会让我变胖,但是也会让我快乐呀~而且,我只有周末的时候才喝一瓶,有问题吗?当然没有,所以分享大家一个道理:

抛开剂量谈药效的都是耍流氓!

所以虽然闭包应用到这里会存在一些内存泄露的问题,但是我这一个单例类就泄露了一个唯一实例的内存,这才哪儿到哪儿,平时少写点奇奇怪怪的代码性能就提升不少了。

当然,上面的代码依然存在问题,一个字:臃肿! 皮裤套棉裤整了一大堆,咱们分析下,现在CreateDiv做了两个事儿:

  1. 判断是否已经存在实例
  2. 在没有实例的情况下去创建实例

为了实现所谓的单一职责原则,我们可以对代码功能进行拆分,拆分后不仅代码可维护性更强,而且功能会更强大、更灵活,怎么搞?往下走!

五、代理的方式实现单例模式

为了复用性强一些,我们把CreateDiv的功能彻底分开,搞一个东西出来,专门负责处理这个唯一实例的问题,再搞一个出来,专门处理创建Div这样的功能问题。

这样岂不是以后再搞单例模式或者再有创建Div这样的需要,我们就可以复用了吗,说干就干!

js 复制代码
const CreateDiv = function (html) {
  this.html = html;
  this.init();
}
CreateDiv.prototype.init = function () {
  const div = document.createElement('div');
  div.innerHTML = this.html;
  document.body.appendChild(div);
}

泼饭!这样CreateDiv就可以专注于创建Div了,再来搞一个专门搞唯一实例的玩意儿。

js 复制代码
// 专注于为CreateDiv类处理唯一实例问题
const proxySingletonCreateDiv = (function () {
  // 利用闭包的特性 创建instance私有变量来控制唯一实例
  let instance = null;
  // 返回一个函数, 用于创建唯一实例
  return function (html) {
    if (!instance) {
      instance = new CreateDiv(html);
    }
    return instance;
  }
})()

const a = new proxySingletonCreateDiv('murlin1');
const b = new proxySingletonCreateDiv('murlin2');

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

呐呐呐,非常完美,这样如果我们需要创建div,就去new CreateDiv(), 因为CreateDiv已经是个最普通的类了,如果需要创建div的同时还要生成唯一实例,只创建一次div的话,就去new proxySingletonCreateDiv()

这样利用代理人proxySingletonCreateDiv解决唯一实例问题的方式就叫缓存代理啦。

六、实践!实践!

接下来,我们就根据一个场景来具体实操一下,比如:

我们在点击一些按钮的时候,需要弹出登录弹框,但是我们需要他只在第一次弹出的时候才去创建这个div并显示出来,并且呢,我们还要考虑到有的时候还可能会有iframe插入及其他弹框的显示情况。

准备好了嘛骚年,我们来利用单例模式来封装并实现一下吧。

首先,我们创建一个getSingleConstruction, 这个方法可以根据某一个构造器来生成一个实现了单例模式的类构造器:

js 复制代码
// 传入一个构造函数,返回的依然是一个构造函数
// 如果构造函数没有实例, 则创建实例, 如果有实例, 则直接返回实例
const getSingleConstruction = function (fn) {
  let instance = null;
  return function () {
    if (!instance) {
      // 如果构造函数没有实例, 则创建实例
      instance = new fn(...arguments);
    }
    return instance;
  }
}

下面是登录框的构造器,关于业务逻辑,大家可以不用太关心:

js 复制代码
// 创建LoginLayer类的根类, 承担业务逻辑
const CreateLoginLayer = function () {
  this.dom = document.createElement('div');
  this.dom.innerHTML = '我是登录浮窗';
  this.dom.style.display = 'none';
  document.body.appendChild(this.dom);
}
CreateLoginLayer.prototype.show = function () {
  this.dom.style.display = 'block';
}
// 根据CreateLoginLayer来创建单例类
const LoginLayer = getSingleConstruction(CreateLoginLayer);

大家可以看到,LoginLayer类就是根据CreateLoginLayer创建出来的单例类,这样无论在任何地方去实例化他,得到的都是唯一的实例了,比如:

js 复制代码
// 点击登录按钮后,弹出登录框
document.getElementById('login-btn').onclick = function () {
  new LoginLayer().show();
}
// 点击个人中心按钮后, 如果没有登录, 则也弹出登录框
document.getElementById('mine-btn').onclick = function () {
  // 模拟判断未登录
  if (!localStorage.getItem('isLogin')) {
    new LoginLayer().show();
  }
}

完美完美,以后只要想弹出登录框,就去执行new LoginLayer().show();就好了,而且,只有在第一次实例化的时候才会去创建实例及div节点,不用担心重复创建dom节点的性能循环相关的问题啦。

而且后续再有任何的需要单例化的类,统统交给getSingleConstruction去处理一下就ok。

js 复制代码
const CreateExampleA = function () {
  this.name = 'exampleA';
}
const CreateExampleB = function () {
  this.name = 'exampleB';
}

const ExampleA = getSingleConstruction(CreateExampleA);
const ExampleB = getSingleConstruction(CreateExampleB);

console.log(new ExampleA() === new ExampleA()); // true 
console.log(new ExampleB() === new ExampleB()); // true

嗨哟,嗨哟,搞定,看到这里是不是成就满满,小小单例,不在话下(叉腰)。

如果看起来不太明白的话,欢迎大家在评论区讨论起来,最好是动手敲一敲呀。

七、总结一下

单例模式是我们学习的第一个设计模式,也了解了关于JavaScript中的单例实现和通用的单例形式,并且还接触到了关于代理模式单一职责原则的,这些我们后面还是会深入再研究滴。

另外,欢迎大家关注我的绿泡泡的GZH:Murlin前端加油站。

哦对了,上一篇在这里:你能看懂的 JavaScript 设计模式!(1)我看看怎么个事儿?

最后,希望大家给个三连呀,感谢大家的关注和点赞转发,再会~

相关推荐
new出一个对象1 小时前
uniapp接入BMapGL百度地图
javascript·百度·uni-app
你挚爱的强哥2 小时前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
y先森3 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy3 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189113 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿4 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡5 小时前
commitlint校验git提交信息
前端
虾球xz5 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇5 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒6 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript