从 ES6 重新认识设计模式之单例模式

1,069 阅读3分钟

介绍

顾名思义单例模式只能拥有唯一的一个实例,即使多次去实例化该类,都会返回第一次实例化后的对象。

模式特点

  • 只有一个实例,全局可访问该实例;
  • 避免重复创建,减少内存占用。

实现单例模式

实现单例模式的方式有很多种,我们依次来看。

构造函数实现

得益于在实例化类时构造函数会自行调用,我们可以在构造函数中向类本身增加一个 instance 的属性,它用于存储实例化后的对象;如果之后有重复实例化的行为都将返回已经存储的实例对象。

这样虽然能够实现单例模式,但会侵入类的自身属性,并且还有一些条件判断的耦合逻辑;所以这种方式并不是完美的实现方式。

class Singleton {
    static instance = null;

    constructor (msg) {
        this.msg = msg;

        return Singleton.instance || (Singleton.instance = this);
    }
}

const One = new Singleton('Hello 🐱!');
const Two = new Singleton('Hello 🐘!');

console.log(One === Two); // true

静态方法实现

如下,我们将 constructor 里面实例化的逻辑提取到静态方法 create 里面;可以调用该静态方法去实例化。

这样同样也可以实现单例模式,但必须是通过 create 去创建的;试想一下,如果去通过 new 去实例化 Singleton 类其实是实现不了单例模式的,而且还会存在一些重复定义的参数;显然这种写法也不太合适。

class Singleton {
    static instance = null;

    constructor (msg) {
        this.msg = msg;
    }

    static create () {
        return this.instance || (this.instance = new Singleton(...arguments));
    }
}

const One = Singleton.create('Hello 🐱!');
const Two = Singleton.create('Hello 🐘!');

console.log(One === Two); // true

闭包实现

我们使用闭包的特性去实现单例模式,首先声明一个自执行函数返回一个函数在其中去实现单例的相关逻辑。

显而易见,这样的方式去实现单例模式相比前面实现的方式更加合理,相关逻辑已经解偶;这样可以使类本身更加聚焦自身的能力。

const Singleton = (function () {
    let instance = null;

    class Singleton {
        constructor (msg) {
            this.msg = msg;
        }
    }

    return function () {
        return instance || (instance = new Singleton(...arguments));
    }
})();

const One = new Singleton('Hello 🐱!');
const Two = new Singleton('Hello 🐘!');

console.log(One === Two); // true

不要高兴的太早,聪明的人已经发现即使我不使用 new 去创建实例他依然是可以的。

const One = Singleton('Hello 🐱!');
const Two = Singleton('Hello 🐘!');

console.log(One === Two); // true

这是因为我们返回的是一个函数,为了更加聚焦面向对象设计的规范;我们使用 new.target 去强约束一下必须使用 new 去实例化。

Tip:new.target 返回构造方法或函数的引用,在普通函数调用中是 undefined 可以用来检测函数或构造方法是否通过 new 运算符被调用。

const Singleton = (function () {
    let instance = null;

    class Singleton {
        constructor (msg) {
            this.msg = msg;
        }
    }

    return function () {
        if (!new.target) {
            throw new Error('must be called with new');
        }

        return instance || (instance = new Singleton(...arguments));
    }
})();

const One = new Singleton('Hello 🐱!');
const Two = new Singleton('Hello 🐘!');

console.log(One === Two); // true

惰性单例模式

惰性的概念在前端领域是非常广泛的,指的是在需要时才去创建或加载;例如图片的懒加载它们都是惰性的。

使用场景

单例模式的使用场景主要用在:

  • 全局状态管理(Vuex、Pinia、Redux);
  • 第三方库也会使用单例模式去管理自身的配置;
  • ...

想必你大概已经明白了单例模式的主要用途,下面我准备了一个 demo 可以简单看看。

全局状态管理

例如我们实现一个 Store 仓库,当然这只是一个 demo,主要为大家方便理解单例模式。

const Store = (function () {
    let instance = null;

    class Store {
        constructor (defaultState = {}) {
            this.state = defaultState;
        }

        get (key) {
            return Reflect.get(this.state, key);
        }

        has () {
            return Reflect.has(this.state, key);
        }

        add (key, payload) {
            this.setState({
                [key]: payload,
            });
        }

        delete (key) {
            const state = this.state;

            Reflect.has(state, key) && Reflect.delete(state, key);

            this.setState(state);
        }

        setState (payload) {
            Object.assign(this.state, payload)
        }
    }

    return function () {
        if (!new.target) {
            throw new Error('must be called with new');
        }

        return instance || (instance = new Store(...arguments));
    }
})();

const One = new Store({
    msg: 'Hello 🐱!',
});
const Two = new Store({
    msg: 'Hello 🐘!',
});

console.log(One === Two); // true