这篇文章只简单通俗的讲单例模式

2,181 阅读8分钟

设计模式得存在让系统代码可重用、可扩展、可解耦、更容易被人理解且保证代码可靠性。设计模式使代码真正工程化。 设计模式是一个庞大而又复杂的体系,单例模式大概是23种设计模式中相对比较简单的一种。今天我们一步一步来解开它的面纱。

了解完高阶函数可能加快我们理解设计模式哟😊,传送门 掘金 | GitHub

设计原则

想要透彻的理解设计模式,你必须先知道我们的6大设计原则

单一职责原则

There should never be more than one reason for a class to change.

简称SRP,核心定义是应该有且仅有一个原因引起类的变更。

里氏替换原则

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T (如果对每一个类型为S的对象o1,都有类型为T的对 象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变 化,那么类型S是类型T的子类型。)

看起来不是很好理解,白话一点就是子类继承父类,单独完全可以运行。只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。

依赖倒置原则

High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions

三层意思:

  1. 高层模块不应该依赖低层模块,两者都应该依赖其抽象。
  2. 抽象不应该依赖细节。
  3. 细节应该依赖抽象。

白话:引用一个对象,如果这个对象有底层类型,直接引用底层类型

接口隔离原则

Clients should not be forced to depend upon interfaces that they don't use.(客户端不应该依赖它不需要的接口。)

白话:每一个接口应该是一种角色

迪米特原则

迪米特法则(Law of Demeter,LoD)也称为最少知识原则(Least Knowledge Principle,LKP):一个对象应该对其他对象有最少的了解

白话:一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的这么多public方法,我就调用这么多,其他的我一概不管

开闭原则

Software entities like classes,modules and functions should be open for extension but closed for modifications.(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。)

白话:对扩展开放,对修改关闭(这个好理解)

简单示例

设计原则我们已经了解完了,接下来我们进入本文得正题,来搞一搞这个单例模式

核心思想

保证一个类只有一个实例,这是单例模式的核心思路。实现方法是先判断实例存在与否,如果存在直接返回,如果不存在就创建了再返回,确保一个类只有一个实例对象。在JavaScript中借助她本身强大的灵活性有多种方式可以实现。单例作为一个命名空间提供者,从全局命名空间里提供一个唯一的访问点来访问该对象。所以我们不难写出下面这种代码

// 单例对象
// 1: 最简单的单例模式 也被称为基本单例模式
const singleton = {
  prop:"value",
  method(){
  }
}
//这种形式的单例模式,所有成员都是公开的,都可以通过singleton来访问。这样的缺点是单例中有一些辅助的方法并不希望暴露给使用者,如果使用者用了这些方法,然后在后面维护的时候,一些辅助方法被删除,这样会造成程序错误。

// 2: 借助闭包我们可以轻易创建一个单例模式 也被称为惰性加载实现单例模式
let Singleton = (function() {
  let instaced;
  function init() {
    console.log('init instance');
    //这里定义单例代码
    return {
      publicMethord: function() {
        console.log("welcome to singleton");
      },
      publicProperty: "test"
    };
  }
  return {
    getInstance: function() {
      if (!instaced) {
        console.log('instance does not exit');
        //确保只有一个实例
        instaced = init(); //使用init方法,是使publicMethod和publicProperty只在要使用的时候才初始化;
      } else {
        console.log('instance already created');
      }
      return instaced;
    }
  };
})();
/*调用公有的方法来获取实例:*/
// 第一次调用
// 单例对象是在调用getInstance的时候才真正被创建
Singleton.getInstance()
// 第二次调用
Singleton.getInstance().publicMethord();

效果如下图所示

作用和注意事项

bb了一顿,还不知道在实际业务中有什么用 下面我们来看看再实际业务中得作用以及注意事项和一个业务中常见得实例

模式作用

  • 模块间通信
  • 系统中某个类的对象只能存在一个
  • 私有属性和方法的保护
  • 明确模块职责

注意事项

  • 注意this的使用
  • 闭包容易造成内存泄露,不需要的要赶快清除
  • 继承时new的成本需要注意
  • 不恰当的使用和增加耦合度。

业务实例

我们来实现一个对话框,无论点击多少次,始终只创建一个实例对象。

  • 第一步创建我们得对话框构造函数
/**
 * 构造器
 * @param {*string} id 
 * @param {*string} html 
 */
let Modal = function(id, html) {
  this.html = html
  this.id = id
  this.domInstance = null  // 私有dom实例
  this.open = false // 是否开启
}

这里我们声明了一个 Modal作为弹框的构造函数并且再其内部定义了公有属性 html、id 和 open。html 用来定义对话框内部的内容,id 用来给弹框定义 id 名称,open 用于判断弹框是否打开。

  • 第二步声明创建私有类的各种方法
// create 方法
Modal.prototype.create = function() {
  if (!this.open) {
    console.log('create dom instance')
    // 构建DOM
    const modal = document.createElement("div");
    modal.innerHTML = this.html;
    modal.id = this.id;
    document.body.appendChild(modal);

    setTimeout(function() {
      modal.classList.add("show");
    }, 0);

    this.open = true;
  }
}
Modal.prototype.hide = function() {
  if (this.open) {
    this.domInstance.classList.add("hide");
    this.open = false
  }
}
// 注意这个delete函数 并不是销毁我们产生的Modal实例,而是销毁页面的DOM实例
Modal.prototype.delete = function() {
  // 删除延迟
  let time = this.open ? 0 : 200
  if (this.domInstance) {
    setTimeout(() => {
      document.body.removeChild(this.domInstance)
      this.domInstance = null
    }, time)
  }
}

在 Modal 的原型链上定义了 create 方法,方法内部我们创建并向 DOM 中插入弹框。定义了 create 方法后我们这里定义隐藏弹框的方法,在其内部给弹框对象添加 hide 类,最后在定义delete方法移除页面上弹框实例。

  • 第三步创建实例方法
// 创建一个Modal实例
let createInstance = (function() {
  /*
  使用闭包来保存当前的实例,这个是单例模式中至关重要的一个部分。
  */
  let instance
  return () => {
    console.log(instance)
    // debugger 用
    if (instance) {
      console.log('已经存在实例了')
    }
    // 判断当前时候还存在以一个实例,如果存在就返回这个实例,不存在的话就生成一个
    return instance || (instance = new Modal("modal", "这是一个单例的模态框"));
  }
})()

我们使用一个闭包来完成对当前实例的保存

  • 第四步封装操作对象
let operate = {
  setModal: null,
  // open
  open() {
    this.setModal = createInstance();
    this.setModal.create();
  },
  // hide
  hide () {
    this.setModal ? this.setModal.hide() : "";
  },
  // 这个操作 delete的是页面中的dom, 而并不是Modal实例
  delete() {
    this.setModal ? this.setModal.delete() : "";
  }
}

这里我们将按钮操作放在 operate 对象里,使得打开和关闭操作可以通过this获取实例setModal。在实际的使用中我们可能只会抛出这个操作对象,然而真正的处理方法调用者并不用知情。

从上面的例子我们可以看出来,实例创建new Modal("modal", "这是一个单例的模态框")之后, 之后的操作仅是对产生的实例对象的行为。

  • 绑定事件进行验证
// test
document.getElementById('open').onclick = function () {
  operate.open();
}
document.getElementById('hide').onclick = function () {
  operate.hide()
}
document.getElementById('delete').onclick = function () {
  operate.delete()
}

在这个例子中注意我们的单例指得并不是页面上的那个模态框, 而是操作我们模态框的Modal实例

总结

单例模式是比较简单常用的一种模式,而且应用也是非常的广泛。按照我个人的理解单例会把各种强耦合的模块组合成一个类,这个类仅仅会提供出一个实例供调用者使用。例如很多对象我们只希望创建一次。比如我们需要显示给用户一个信息页面,内容不变但是用户会多次点击。这个页面无论用户点击多少次,我们只需要创建一次。那这种情况下就非常的适合使用单例模式。上面得这个示例我相信大部分前端工程师10分钟就能写的很完美。为了演示单例模式的应用我才这么写,在实际得业务中,我们大家一般不会去那么写。

设计模式在我看来是一把双刃剑,用的好的话代码结构清晰,可以实现理想的高内聚低耦合。用的不好的话代码可能会变得一塌糊涂,可读性和可维护性基本为0。至于到底要不要用,那就见仁见智了。

设计模式代码地址

文章代码地址

原文地址 如果觉得有用得话给个⭐吧