网络日志

JavaScript 设计模式 —— 单例模式

如果觉得文章不错,欢迎关注、点赞和分享!持续分享技术博文,关注微信公众号 👉🏻 前端LeBron

在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案 —— 设计模式设计模式原则:“找出程序中变化的地方,并将变化封装起来”

为什么要学习设计模式?

从事开发岗位也有一年多的时间了,见识过陈年老项目,也从零到一搭建过一个项目。随着项目和业务的不断扩张,写下的代码如果没有进行设计,就渐渐变成了 emm ... x 山,怎么写都不对劲,过段时间就想着重构。

人之所以可以走到食物链的顶端,是因为会学习、总结,会使用“名字”和“工具”。而设计模式并不是凭空发明出来的,是经过了大量的项目实践总结出来的对某种业务场景下的程序编写最佳实践,总结出来的解决方案,并给它取了个名字,就变成了一个设计模式。就好比篮球场上的战术,组织后卫常喊出打几号战术,而不是一个人一个人地指挥,简洁的代号往往比冗杂的描述更优雅。有可能你经常写程序的一种方式,可以描述出来,但不知道它叫什么名字,有可能就是一种设计模式。所以我们在学习的过程中,经常会有这种感受: ooooo !!这玩意儿我经常用,经常这么写,原来这个是 xx 模式!

最后,为什么要学设计模式呢?虽然有时候设计模式会使代码复杂度升高,增加开发的成本,但极大地降低了维护成本。就好比图书馆中的书如果无序地散落在各个角落,找起来如同大海捞针;而如果标号且归类、有序地放在指定书架上,找起来就容易了许多。想象一个场景,某天,你指着一段代码开始骂,这谁写的 x 山!鼠标移了上去, git 修改记录显示,哦!我写的,那没事了。赶紧设计模式学起来,优雅地编写简洁、可复用、易维护的程序 ~

单例模式

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

2、主要解决一个全局使用的类频繁地创建和销毁,占用内存

实现单例模式

实现一个简洁的单例模式并不复杂,无非就是将实例对象保存起来,下一次获取/创建对象的时候,直接返回之前创建的实例

最简单例模式

  • 闭包实现
  • ES6 实现
// 利用闭包实现
var Singleton = (function () {
  var instance;
  function Instance() {}
  return function () {
    if (!instance) {
      instance = new Instance();
    }
    return instance;
  };
})();

var single1 = Singleton();
var single2 = Singleton();

console.log(single1 === single2); // true


// ES6实现
class Singleton {
  constructor() {
    if (!Singleton.instance) {
      Singleton.instance = this;
    }
    return Singleton.instance;
  }
}

const single1 = new Singleton();
const single2 = new Singleton();

console.log(single1 === single2); // true

惰性单例

惰性单例:初始化时不进行实例创建,到获取实例时才进行实例创建,类似前端的资源懒加载思想
// 非惰性单例
class Tool{}
class SingleTon{
  constructor (name,...args) {
    this.name = name
    if(!SingleTon.instance){
      SingleTon.instance = new Tool(args)
    }
  }

  getName(){
    return  this.name;
  }

  static  getInstance(){
    return  this.instance;
  }
}


// 惰性单例
class Tool{}
class SingleTon{
  constructor (name) {
    this.name = name
  }

  getName(){
    return  this.name;
  }

  static  getInstance(){
    if(!this.instance){
      this.instance = new Tool(arguments)
    }
    return  this.instance;
  }
}

透明单例和代理单例

  • 透明单例

    • 将实例的创建和方法的执行封装到了一个类中
    • 不太符合“单一职责原则”,代码不易读、不易改
  • 代理单例

    • 代理模式:将实例创建和方法分离,委托出去
    • Person 作为普通类,可以创建对象实例
    • createPerson 作为单例创建代理,可以创建一个单例
    • 符合“单一职责原则”,代码易读、易改
 // 透明 单例
class createPerson{
  constructor (name) {
    if(createPerson.instance){
      return createPerson.instance;
    }
    this.name = name;
    this.init();
    return createPerson.instance = this;
  }

  init(){
    console.log('My name is ',this.name)
  }
}
const p1 = new createPerson('lebron')
const p2 = new createPerson('james')
p1.init(); // My name is  lebron
p2.init(); // My name is  lebron


// 代理单例
class Person{
  constructor (name) {
    this.name = name;
  }
  init(){
    console.log('My name is ',this.name)
  }
}

class createPerson{
  constructor (name) {
    if(!createPerson.instance){
      createPerson.instance = new Person(name)
    }
    return createPerson.instance;
  }
}
const p1 = new createPerson('lebron')
const p2 = new createPerson('james')
p1.init(); // My name is  lebron
p2.init(); // My name is  lebron

最终版本

每次实现一个类的单例模式都去CV重复的模板代码,不太符合预期。

于是,我们可以根据设计模式的原则“找出程序变化的地方,并将变化封装起来”

可以做如下改造

class SingleTon{
  constructor (fn) {
    let singleInstance;
    function singleConstructor(...args){
      // 第一次实例化
      if(!singleInstance){
        singleInstance = new fn(...args);
      }
      // 多次实例化直接返回
      return singleInstance;
    }

    singleConstructor.prototype = Object.create(fn.prototype);
    return singleConstructor;
  }
}

class Person{
  constructor (name) {
    this.name = name;
  }
  init(){
    console.log('My name is ',this.name)
  }
}
const createPerson = new SingleTon(Person);
const p1 = new createPerson('lebron');
const p2 = new createPerson('james');
p1.init(); // My name is  lebron
p2.init(); // My name is  lebron
console.log(p1 === p2)  // true

class Tool{
  constructor (number) {
    this.number = number;
  }

  init(){
    console.log('This is tool ', this.number)
  }
}
const createTool = new SingleTon(Tool);
const t1 = new createTool(1);
const t2 = new createTool(2);
t1.init(); // This is tool  1
t2.init(); // This is tool  1
console.log(t1 === t2); // true

单例模式的应用场景

登录窗口

登录窗口在我们的软件中很常见,也很适合使用单例模式创建(全局唯一、频繁使用)

使用单例模式创建可以避免多次节点创建和销毁

class Login {
  constructor() {
    this.element = document.createElement('div');
    this.element.innerHTML = `
           用户名 <input name="username"/>
           密码 <input name="password"/>
           <input type="submit" value="登录"/>
          `;
    this.element.style.cssText = `width:100px;height:100px;position:absolute;left:50%;top:50%;margin-top:-50px;margin-left:-50px;display:none`;
    document.body.appendChild(this.element);
  }
  show() {
    this.element.style.display = 'block';
    console.log('show');
  }
  hide() {
    this.element.style.display = 'none';
    console.log('hide');
  }
}

// 采用上文最终版本创建单例代码
const createLogin = new SingleTon(Login);
const loginInstance = createLogin();

document.getElementById('show-btn').addEventListener('click', function () {
  loginInstance.show();
});
document.getElementById('hide-btn').addEventListener('click', function () {
  loginInstance.hide();
});

Store

全局状态管理,全局唯一实例,例如 Vuex / Redux / Mobx
// 已省略一些无关代码
if (!Vue && typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

function install (_Vue) {
  if (Vue && _Vue === Vue) {
    console.error('[vuex] already installed. Vue.use(Vuex) should be called only once.')
    return
  }
  Vue = _Vue
}

Jquery

Jquery 也是应用的单例模式,虽然它已经渐渐淡出了我们的视野 😂
if(window.jQuery!=null){
  return window.jQuery;
}else{
    //init~~~~~~~
}

总结

使用场景

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

优点

  • 在内存里只有一个实例,减少了内存的开销,尤其是频繁地创建和销毁实例
  • 避免对资源的多重占用

闲谈

上半年一直在忙毕业的事情,然后也休息了一段时间,已经大半年没写技术文章了。

近期正式入职了,开始逐步调整到一个比较好的学习和工作状态。制定一些学习计划,恢复更新文章(很久没写还是有点不太习惯哈哈

第一个系列《设计模式》第一篇墨迹了半天,但也终于产出了。后续会持续更新,更新频率和进度看空闲时间情况了哈哈,欢迎关注~

一个还在努力的前端工程师,继续加油!

持续分享技术博文,欢迎关注!