单列模式

单例模式

单例模式多是设计模式里面最简单的模式了,虽然简单,但在咱们平常生活和编程中却常常接触到,本节咱们一块儿来学习一下。
单例模式 (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)    // true
console.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)    // true
console.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 的时候才进行。前端

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

  • 懒汉式单例是在使用时才实例化
  • 饿汉式是当程序启动时或单例模式类一加载的时候就被建立。
  • 咱们能够举一个简单的例子比较一下:
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)    // true
console.log(visitor3 === visitor4)    // true
ts实现
  • 懒汉式单例react

    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 private
    
    let 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 的实例。vue-router

下面咱们能够看看 ElementUI 2.9.2 的源码是如何实现的,为了观看方便,省略了部分代码:
import Vue from 'vue'
import loadingVue from './loading.vue'

const LoadingConstructor = Vue.extend(loadingVue)

let fullscreenLoading

const 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 资源占用更少;
单例模式也是有缺点的
  • 单例模式对扩展不友好,通常不容易扩展,由于单例模式通常自行实例化,没有接口
  • 在并发测试中,单例模式不利于代码调试。在调试过程当中,若是单例中的代码没有执行完,也不能模拟生成一个新的对象。
  • 与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化;
单例模式的使用场景那咱们应该在什么场景下使用单例模式呢:
  • 当一个类的实例化过程消耗的资源过多,可使用单例模式来避免性能浪费;
  • 当项目中须要一个公共的状态,那么须要使用单例模式来保证访问一致性
相关文章
相关标签/搜索