单例模式

单例模式可能是设计模式外面最简略的模式了,尽管简略,但在咱们日常生活和编程中却常常接触到,本节咱们一起来学习一下。
单例模式 (Singleton Pattern)又称为单体模式,保障一个类只有一个实例,并提供一个拜访它的全局拜访点。也就是说,第二次应用同一个类创立新对象的时候,应该失去与第一次创立的对象完全相同的对象。

1.你已经遇见过的单例模式

  • 当咱们在电脑上玩经营类的游戏,通过一番目迷五色的骚操作好不容易走上正轨,夜深了咱们去劳动,第二天关上电脑,发现要从头玩,立马就把电脑扔窗外了,所以个别心愿从前一天的进度接着打,这里就用到了存档。每次玩这游戏的时候,咱们都心愿拿到同一个存档接着玩,这就是属于单例模式的一个实例。
  • 编程中也有很多对象咱们只须要惟一一个,比方数据库连贯、线程池、配置文件缓存、浏览器中的 window/document 等,如果创立多个实例,会带来资源消耗重大,或拜访行为不统一等状况。
  • 相似于数据库连贯实例,咱们可能频繁应用,然而创立它所须要的开销又比拟大,这时只应用一个数据库连贯就能够节约很多开销。一些文件的读取场景也相似,如果文件比拟大,那么文件读取就是一个比拟重的操作。比方这个文件是一个配置文件,那么齐全能够将读取到的文件内容缓存一份,每次来读取的时候拜访缓存即可,这样也能够达到节约开销的目标。
在相似场景中,这些例子有以下特点:
  • 每次访问者来拜访,返回的都是同一个实例;
  • 如果一开始实例没有创立,那么这个特定类须要自行创立这个实例;

2. 实例的代码实现

  • 如果你是一个前端er,那么你必定晓得浏览器中的 window 和 document 全局变量,这两个对象都是单例,任何时候拜访他们都是一样的对象,window 示意蕴含 DOM 文档的窗口,document 是窗口中载入的 DOM 文档,别离提供了各自相干的办法。
  • 在 ES6 新增语法的 Module 模块个性,通过 import/export 导出模块中的变量是单例的,也就是说,如果在某个中央扭转了模块外部变量的值,别的中央再援用的这个值是扭转之后的。除此之外,我的项目中的全局状态管理模式 Vuex、Redux、MobX 等保护的全局状态,vue-router、react-router 等保护的路由实例,在单页利用的单页面中都属于单例的利用(但不属于单例模式的利用。
  • 在 JavaScript 中应用字面量形式创立一个新对象时,实际上没有其余对象与其相似,因为新对象曾经是单例了:
  • 那么问题来了,如何对构造函数应用 new 操作符创立多个对象时,仅获取同一个单例对象呢。
  • 对于刚刚打经营游戏的例子,咱们能够用 JavaScript 来
    实现一下:
  function ManageGame(){      if(ManageGame._schedule){  // 判断是否曾经有单例了            return ManageGame._schedule      }      ManageGame._schedule = this  }  ManageGame.getInstance = function(){      if(ManageGame._schedule){  // 判断是否曾经有单例了            return ManageGame._schedule      }      return ManageGame ._schedule =new ManageGame()  }  const schedule1 = new ManageGame()  const schedule2 =ManageGame.getInstance()  console.log(schedule1===schedule2)
ts的 class 革新
   class ManageGame{        private static schedule: any = null;        static getInstance() {            if (ManageGame.schedule) {        // 判断是否曾经有单例了                return ManageGame.schedule            }            return ManageGame.schedule = new ManageGame()        }        constructor() {            if (ManageGame.schedule) {        // 判断是否曾经有单例了                return ManageGame.schedule            }            ManageGame.schedule = this        }    }    const schedule1 = new ManageGame()    const schedule2 = ManageGame.getInstance()    console.log(schedule1 === schedule2)// true
毛病:下面办法的毛病在于保护的实例作为动态属性间接裸露,内部能够间接批改。

3. 单例模式的通用实现

依据下面的例子提炼一下单例模式,游戏能够被认为是一个特定的类(Singleton),而存档是单例(instance),每次拜访特定类的时候,都会拿到同一个实例。次要有上面几个概念:
  • Singleton :特定类,这是咱们须要拜访的类,访问者要拿到的是它的实例;
  • instance :单例,是特定类的实例,特定类个别会提供 getInstance 办法来获取该单例;
  • getInstance :获取单例的办法,或者间接由 new 操作符获取;

3.1 IIFE形式创立单列模式

  • 简略实现中,咱们提到了毛病是实例会裸露,那么这里咱们首先应用立刻调用函数 IIFE 将不心愿公开的单例实例 instance 暗藏。
  • 当然也能够应用构造函数复写将闭包进行的更彻底
const Singleton = (function() {    let _instance = null        // 存储单例        const Singleton = function() {        if (_instance) return _instance     // 判断是否已有单例        _instance = this        this.init()                         // 初始化操作        return _instance    }        Singleton.prototype.init = function() {        this.foo = 'Singleton Pattern'    }        return Singleton})()const visitor1 = new Singleton()const visitor2 = new Singleton()console.log(visitor1 === visitor2)    // true
  • 这样一来,尽管仍应用一个变量 _instance 来保留单例,然而因为在闭包的外部,所以内部代码无奈间接批改。
  • 在这个根底上,咱们能够持续改良,减少 getInstance 静态方法:
const Singleton = (function() {    let _instance = null        // 存储单例        const Singleton = function() {        if (_instance) return _instance     // 判断是否已有单例        _instance = this        this.init()                         // 初始化操作        return _instance    }        Singleton.prototype.init = function() {        this.foo = 'Singleton Pattern'    }        Singleton.getInstance = function() {        if (_instance) return _instance        _instance = new Singleton()        return _instance    }        return Singleton})()const visitor1 = new Singleton()const visitor2 = new Singleton()         // 既能够 new 获取单例const visitor3 = Singleton.getInstance() // 也能够 getInstance 获取单例console.log(visitor1 === visitor2)    // trueconsole.log(visitor1 === visitor3)    // true
  • 代价和上例一样是闭包开销,并且因为 IIFE 操作带来了额定的复杂度,让可读性变差。
  • IIFE 外部返回的 Singleton 才是咱们真正须要的单例的构造函数,内部的 Singleton 把它和一些单例模式的创立逻辑进行了一些封装。
  • IIFE 形式除了间接返回一个办法/类实例之外,还能够通过模块模式的形式来进行,就不贴代码了,代码实现在 Github 仓库中,读者能够本人瞅瞅。

3.2 块级作用域形式创立单例

let getInstance{    let _instance = null        // 存储单例        const Singleton = function() {        if (_instance) return _instance     // 判断是否已有单例        _instance = this        this.init()                         // 初始化操作        return _instance    }        Singleton.prototype.init = function() {        this.foo = 'Singleton Pattern'    }        getInstance = function() {        if (_instance) return _instance        _instance = new Singleton()        return _instance    }}const visitor1 = getInstance()const visitor2 = getInstance()console.log(visitor1 === visitor2)

3.3 单例模式赋能

之前的例子中,单例模式的创立逻辑和原先这个类的一些性能逻辑(比方 init 等操作)混淆在一起,依据繁多职责准则,这个例子咱们还能够持续改良一下,将单例模式的创立逻辑和特定类的性能逻辑拆开,这样性能逻辑就能够和失常的类一样。
/* 性能类 */class FuncClass {    constructor(bar) {         this.bar = bar        this.init()    }        init() {        this.foo = 'Singleton Pattern'    }}/* 单例模式的赋能类 */const Singleton = (function() {    let _instance = null        // 存储单例        const ProxySingleton = function(bar) {        if (_instance) return _instance     // 判断是否已有单例        _instance = new FuncClass(bar)        return _instance    }        ProxySingleton.getInstance = function(bar) {        if (_instance) return _instance        _instance = new Singleton(bar)        return _instance    }        return ProxySingleton})()const visitor1 = new Singleton('单例1')const visitor2 = new Singleton('单例2')const visitor3 = Singleton.getInstance()console.log(visitor1 === visitor2)    // trueconsole.log(visitor1 === visitor3)    // true
  • 这样的单例模式赋能类也可被称为代理类,将业务类和单例模式的逻辑解耦,把单例的创立逻辑形象封装进去,有利于业务类的扩大和保护。代理的概念咱们将在前面代理模式的章节中更加具体地探讨。
  • 应用相似的概念,配合 ES6 引入的 Proxy 来拦挡默认的 new 形式,咱们能够写出更简化的单例模式赋能办法:
/* Person 类 */class Person {    constructor(name, age) {        this.name = name        this.age = age    }}/* 单例模式的赋能办法 */function Singleton(FuncClass) {    let _instance    return new Proxy(FuncClass, {        construct(target, args) {            return _instance || (_instance = Reflect.construct(FuncClass, args)) // 应用 new FuncClass(...args) 也能够        }    })}const PersonInstance = Singleton(Person)const person1 = new PersonInstance('张小帅', 25)const person2 = new PersonInstance('李小美', 23)console.log(person1 === person2)    // true

4. 惰性单例、懒汉式-饿汉式

有时候一个实例化过程比拟消耗性能的类,然而却始终用不到,如果一开始就对这个类进行实例化就显得有些节约,那么这时咱们就能够应用惰性创立,即提早创立该类的单例。之前的例子都属于惰性单例,实例的创立都是 new 的时候才进行。

惰性单例又被成为懒汉式,绝对应的概念是饿汉式:

  • 懒汉式单例是在应用时才实例化
  • 饿汉式是当程序启动时或单例模式类一加载的时候就被创立。
  • 咱们能够举一个简略的例子比拟一下:
class FuncClass {    constructor() { this.bar = 'bar' }}// 饿汉式const HungrySingleton = (function() {    const _instance = new FuncClass()        return function() {        return _instance    }})()// 懒汉式const LazySingleton = (function() {    let _instance = null        return function() {        return _instance || (_instance = new FuncClass())    }})()const visitor1 = new HungrySingleton()const visitor2 = new HungrySingleton()const visitor3 = new LazySingleton()const visitor4 = new LazySingleton()console.log(visitor1 === visitor2)    // trueconsole.log(visitor3 === visitor4)    // true
ts实现
  • 懒汉式单例

    class LazySingleton{ private static instance:LazySingleton = null; private constructor(){     //private 防止类在内部被实例化 } public static  getInstance():LazySingleton{      if (LazySingleton.instance == null) {         LazySingleton.instance = new LazySingleton();   }   return LazySingleton.instance; }  someMethod() {}}let someThing = new LazySingleton(); // Error: constructor of 'singleton' is privatelet instacne = LazySingleton.getInstance(); // do some thing with the instance
用懒汉式单例模式模仿产生美国当今总统对象。
剖析:在每一届任期内,美国的总统只有一人,所以本实例适宜用单例模式实现,图 2 所示是用懒汉式单例实现的结构图。
        class SingletonLazy{        public static main(arg) {            let zt1 = President.getInstance();            zt1.getName();            let zt2 = President.getInstance();            zt2.getName();            if (zt1 === zt2) {                console.log("他们是同一个人");            } else {                console.log("他们不是同一人");            }        }    }    class President{        private static instance: President = null;        private constructor() {            console.log("产生一个总统了");        }        public static  getInstance():President{            if (President.instance == null) {                President.instance = new President();            } else {                console.log("曾经有了一个总统了,不能产生新总统!");            }            return President.instance;        }        public  getName():void {          console.log("我是美国总统:特朗普。");        }            }
  • 饿汉式单例
namespace 饿汉式{    class HungrySingleton{        private static instance: HungrySingleton = new HungrySingleton();        private constructor() {                    }        public static getInstance(): HungrySingleton{            return HungrySingleton.instance;        }    }        let someThing = new HungrySingleton(); // Error: constructor of 'singleton' is private        let instacne = HungrySingleton.getInstance(); // do some thing with the instance}

5. 源码中的单例模式

以 ElementUI 为例,ElementUI 中的全屏 Loading 蒙层调用有两种模式:
// 1. 指令模式Vue.use(Loading.directive)// 2. 服务模式Vue.prototype.$loading = service

用服务形式应用全屏 Loading 是单例的,即在前一个全屏 Loading 敞开前再次调用全屏 Loading,并不会创立一个新的 Loading 实例,而是返回现有全屏 Loading 的实例。

上面咱们能够看看 ElementUI 2.9.2 的源码是如何实现的,为了观看不便,省略了局部代码:
import Vue from 'vue'import loadingVue from './loading.vue'const LoadingConstructor = Vue.extend(loadingVue)let fullscreenLoadingconst Loading = (options = {}) => {    if (options.fullscreen && fullscreenLoading) {        return fullscreenLoading    }    let instance = new LoadingConstructor({        el: document.createElement('div'),        data: options    })    if (options.fullscreen) {        fullscreenLoading = instance    }    return instance}export default Loading

6. 单例模式的优缺点

单列模式的特点
  • 单例类只有一个实例对象;
  • 该单例对象必须由单例类自行创立;
  • 单例类对外提供一个拜访该单例的全局拜访点。
单例模式次要解决的问题就是节约资源,放弃拜访一致性。
简略剖析一下它的长处:
  • 单例模式在创立后在内存中只存在一个实例,节约了内存开销和实例化时的性能开销,特地是须要重复使用一个创立开销比拟大的类时,比起实例一直地销毁和从新实例化,单例能节约更多资源,比方数据库连贯;
  • 单例模式能够解决对资源的多重占用,比方写文件操作时,因为只有一个实例,能够防止对一个文件进行同时操作;
    *只应用一个实例,也能够减小垃圾回收机制 GC(Garbage Collecation) 的压力,体现在浏览器中就是零碎卡顿缩小,操作更晦涩,CPU 资源占用更少;
单例模式也是有毛病的
  • 单例模式对扩大不敌对,个别不容易扩大,因为单例模式个别自行实例化,没有接口
  • 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模仿生成一个新的对象。
  • 与繁多职责准则抵触,一个类应该只关怀外部逻辑,而不关怀里面怎么样来实例化;
单例模式的应用场景那咱们应该在什么场景下应用单例模式呢:
  • 当一个类的实例化过程耗费的资源过多,能够应用单例模式来防止性能节约;
  • 当我的项目中须要一个公共的状态,那么须要应用单例模式来保障拜访一致性