跳转到内容

单例模式

首发于:2022-03-22

基本概念

单例模式 (Singleton Pattern)又称为单体模式,保证一个类只有一个实例,并提供一个访问它的全局访问点。也就是说,第二次使用同一个类创建新对象的时候,应该得到与第一次创建的对象完全相同的对象。

现实生活中的例子

现实中单例的例子很多,比如:

  • 国家主席:选一个国家主席,已经选出来了,就不能再选了。

  • 领结婚证:没结婚可以领结婚证,结婚了就不能再领了。

应用场景

单例模式的应用场景通常具有下面两种特点:

  1. 全局有且只能有一个这样的对象,像一些公共对象,需要用单例来保证访问的一致性,前端比较有代表性的就是 window/document 对象,模态弹窗等;

  2. 实例化消耗资源比较多,可以使用单例模式来避免性能浪费,比如数据库连接、配置文件缓存等;

优缺点

单例模式主要解决的问题就是节约资源,保持访问一致性

优点:

  1. 单例模式在创建后在内存中只存在一个实例,节约了内存开支和实例化时的性能开支,特别是需要重复使用一个创建开销比较大的类时,比起实例不断地销毁和重新实例化,单例能节约更多资源,比如数据库连接;
  2. 单例模式可以解决对资源的多重占用,比如写文件操作时,因为只有一个实例,可以避免对一个文件进行同时操作;
  3. 只使用一个实例,也可以减小垃圾回收机制 GC(Garbage Collecation) 的压力,表现在浏览器中就是系统卡顿减少,操作更流畅,CPU 资源占用更少;

缺点:

  1. 单例模式对扩展不友好,一般不容易扩展,因为单例模式一般自行实例化,没有接口;
  2. 与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化;

实现

常见简单实现

js
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) // true

class 语法实现

js
class ManageGame {
    static _schedule = 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

上述方式实现的单例有个缺点就是都会暴露实例本身,外部可以直接修改。

通用实现

js
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);

这种实现方式用闭包对内部实例进行的了保护,外部代码无法修改,但是代价就是闭包会造成额外的开销,以及代码的复杂度增加,可读性降低。

利用块级作用域实现:

js
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) // true

这是用块级作用域的方式来隐藏内部变量。

单例赋能

前面的例子中,我们在使用单例模式的时候每次都要写一大堆,而且里面可能还有这个类的一些初始化的业务逻辑,这并不符合单一职责原则。所以我们需要将单例模式的创建逻辑和特定类的业务逻辑拆开,这样功能逻辑就可以和正常的类一样。

js
/* 功能类 */
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

上面的代码使用 Proxy 来拦截默认的 new 方式,可以简化上面的代码为:

js
/* Person 类 */
class Person {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
}

/* 单例模式的赋能方法 */
function Singleton(FuncClass) {
    let _instance
    return new Proxy(FuncClass, {
        construct(target, args) {
            // Reflect.construct(FuncClass, args) 等同于 new FuncClass(...args)
            return _instance || (_instance = Reflect.construct(FuncClass, args))
        }
    })
}

const PersonInstance = Singleton(Person)

const person1 = new PersonInstance('张小帅', 25)
const person2 = new PersonInstance('李小美', 23)

console.log(person1 === person2) // true

懒汉式与饿汉式

有时候一个实例化过程比较耗费性能的类,但是却一直用不到,如果一开始就对这个类进行实例化就显得有些浪费,那么这时我们就可以使用惰性创建,即延迟创建该类的单例。

懒汉式又被称为惰性单例,与饿汉式是相对的:

  • 懒汉式单例是在使用时才实例化
  • 饿汉式是当程序启动时或单例模式类一加载的时候就被创建
js
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

京ICP备18043750号