美文网首页js css htmlTypeScript基础
【TS】另一种实现typescript单例模式的方式(支持代码提

【TS】另一种实现typescript单例模式的方式(支持代码提

作者: 来一斤BUG | 来源:发表于2023-11-07 10:40 被阅读0次

我之前写过一个ts单例模式的基类(传从门:实现一个ts单例模式基类(支持代码提示、禁止二次实例化) - 简书 (jianshu.com))。但是经过我思考以后,觉得还有另一种方式创建通用的单例模式。
那么先上代码:

/**
 * 单例类的创建器
 * @param cls 需要单例化的类
 * @example const AClass = singleton(class { ... });
 */
function singleton<T extends { new(...args: any[]): {}, prototype: any }>(cls: T): T & { instance: T["prototype"] } {
    // 实例
    let instance: any = null;
    // 构造函数代理
    let constructorProxy = null;
    const proxy = new Proxy(
        cls,
        {
            construct(target: any, argArray: any[], newTarget: any): T {
                if (!instance) {
                    instance = new cls(...argArray);
                    // 下面这一行用于替换掉construct函数,减少instance判断,也可以删去这行代码
                    this.construct = (target: any, argArray: any[], newTarget: any): T => instance;
                }
                return instance;
            },
            get(target: T, p: string | symbol, receiver: any): any {
                if (p === "instance") {
                    return new proxy();
                }
                if (p === "prototype") {
                    // 用于阻止通过new SampleClass.prototype.constructor()创建新对象
                    constructorProxy = constructorProxy ?? new Proxy(target[p], {
                        get(target: any, p: string | symbol, receiver: any): any {
                            if (p === "constructor") {
                                return proxy;
                            }
                            return target[p];
                        },
                    });
                    return constructorProxy;
                }
                return target[p];
            },
            set(target: T, p: string | symbol, newValue: any, receiver: any): boolean {
                if (p === "instance") {
                    return false;
                }
                target[p] = newValue;
                return true;
            },
        },
    );
    return proxy as T & { instance: T["prototype"] }; // 这里最好写将proxy的类型转换成函数签名的返回类型(T & { instance: T["prototype"] }),不然在某些环境中可能会出现错误
}

由于我们的singleton不是类,而是普通的函数,我们在使用的时候就需要传入一个类,并且用一个变量接收返回值。
示例代码:

const SampleClass = singleton(class {
    
    static sampleStaticFunc() {
        console.log("sampleStaticFunc");
    }
    
    sampleFunc() {
        console.log("sampleFunc");
    }
    
});

console.log("new SampleClass() === new SampleClass():", new SampleClass() === new SampleClass());
console.log("SampleClass.instance === new SampleClass():", SampleClass.instance === new SampleClass());
console.log("SampleClass.instance === SampleClass.instance:", SampleClass.instance === SampleClass.instance);
console.log("new (SampleClass.prototype.constructor as typeof SampleClass)() === SampleClass.instance:", new (SampleClass.prototype.constructor as typeof SampleClass)() === SampleClass.instance);
SampleClass.instance.sampleFunc();
SampleClass.sampleStaticFunc();

控制台打印:

new SampleClass() === new SampleClass(): true
SampleClass.instance === new SampleClass(): true
SampleClass.instance === SampleClass.instance: true
new (SampleClass.prototype.constructor as typeof SampleClass)() === SampleClass.instance: true
sampleFunc
sampleStaticFunc
多亏ts类型系统的帮助,我们保留了代码提示的功能 成员属性提示 静态属性提示

与单例模式基类不同的是,本文的方式通过函数调用返回一个代理对象(Proxy)。利用Proxy我们可以阻止外部直接访问类。
Proxy的第二个参数对象中可以编写construct陷阱函数,用于拦截new操作符,下面是construct的函数签名:

interface ProxyHandler<T extends object> {
    /**
     * A trap for the `new` operator.
     * @param target The original object which is being proxied.
     * @param newTarget The constructor that was originally called.
     */
    construct?(target: T, argArray: any[], newTarget: Function): object;

    // 省略了其他的定义
}

当代理拦截到企图利用new创建新对象时,如果是第一次实例化,那么允许创建对象;反之返回之前创建的对象。这样可以防止多次实例化:

construct(target: any, argArray: any[], newTarget: any): T {
    if (!instance) {
        instance = new cls(...argArray);
        // 下面这一行用于替换掉construct函数,减少instance判断,也可以删去这行代码
        this.construct = (target: any, argArray: any[], newTarget: any): T => instance;
    }
    return instance;
},

为了支持SampleClass.instance方式获取实例,我们可以在get陷阱函数中返回instance对象。我这里直接使用了new proxy(),让construct代替我们返回instance对象:

get(target: T, p: string | symbol, receiver: any): any {
    if (p === "instance") {
        return new proxy();
    }
    return target[p];
}

同时在set函数中阻止对instance赋值

set(target: T, p: string | symbol, newValue: any, receiver: any): boolean {
    if (p === "instance") {
        return false;
    }
    target[p] = newValue;
    return true;
}

以上做法还是不足以完全拦截多次实例化,通过new (SampleClass.prototype.constructor as any)()还是可以再次创建新对象。那么我们还需要对SampleClass.prototype.constructor进行代理。做法是将前面提到的get陷阱函数改成以下代码:

get(target: T, p: string | symbol, receiver: any): any {
    if (p === "instance") {
        return new proxy();
    }
    if (p === "prototype") {
        // 用于阻止通过new SampleClass.prototype.constructor()创建新对象
        // constructorProxy定义在了代理之外、singleton之中,可以参考前面的完整代码
        constructorProxy = constructorProxy ?? new Proxy(target[p], {
            get(target: any, p: string | symbol, receiver: any): any {
                if (p === "constructor") {
                    return proxy;
                }
                return target[p];
            },
        });
        return constructorProxy;
    }
    return target[p];
}

写完了逻辑相关的代码,我们再来写点类型相关的代码。

function singleton<T extends { new(...args: any[]): {}, prototype: any }>(cls: T): T & { instance: T["prototype"] };

对于上面这个函数签名,<T extends { new(...args: any[]): {}, prototype: any }>(cls: T)表示需要传入的参数需要有构造函数和原型属性,也就是一个类,且不限制构造函数的参数个数和类型。函数的返回值类型首先需要返回cls类的类型,也就是T,但是这样ts类型系统无法知道里面有instance属性,所以这里需要改成交叉类型,而且instance的类型需要为cls类的原型,结果就是T & { instance: T["prototype"] }。简单来说,T表示了类中有哪些静态属性,而T["prototype"]表示类中有哪些成员属性。

以上的方法有以下优缺点:

优点:

  • 保留了代码提示;
  • 依然可以使用new SampleClass(),只不过会得到之前创建过的实例;
  • 可以直接使用SampleClass.instance属性获取实例,而不一定得使用SampleClass.getInstance()方法;
  • 保留了类唯一一次宝贵的继承机会,不用因为继承单例模式基类而无法继承其他类;

缺点:

  • 无法再对构造函数使用protectedprivate访问限定符;
  • 使用SampleClass.instance的方式获取实例时无法对构造函数进行传参,但是通过new操作符可以在第一次实例化的时候传参,有可能导致意想不到的问题,建议不要使用构造函数参数;
  • 使用const SampleClass = singleton(class { ... });创建类的方式不太常用,比较奇怪;
  • IDE不再主动将SampleClass当成一个类了,它的类型和在编辑器中的样式将有别于普通的类; 单例化的类 普通的类
  • 无法在同一行中使用默认导出了,需要另起一行进行默认导出,影响不大;
  • 如果使用var的方式定义SampleClass变量,会产生变量提升的问题,在var定义之前使用SampleClassundefined。如果用let或者const定义就不会有变量提升的问题,会直接报错:error TS2448: Block-scoped variable 'SampleClass' used before its declaration.。这里我更建议使用const
  • IDE有可能无法实时提示private 成员不可访问protected 成员不可访问

相关文章

  • 单例模式的常用实现方式

    单例模式属于最常用的设计模式,Java中有很多实现单例模式的方式,各有其优缺点 实现方式对比 单例实现方式线程安全...

  • 单例模式

    单例模式及C++实现代码单例模式4种实现详解 c++11改进我们的模式之改进单例模式 单例模式(Singleton...

  • C++ 单例模式

    本文介绍C++单例模式的集中实现方式,以及利弊 局部静态变量方式 上述代码通过局部静态成员single实现单例类,...

  • 设计模式--单例模式

    单例模式概述 单例模式实现方式 为什么要使用单例模式 单例模式实现方式 饿汉式 类加载后就会将对象加载到内存中,保...

  • kotlin实现单例模式

    1.懒汉式实现单例模式 2.线程安全懒汉式实现单例模式 3.双重校验懒汉式实现单例模式 4.静态内部类方式实现单例模式

  • 单例模式,你真的写对了吗?

    看公司代码的时候发现项目中单例模式应用挺多的,并且发现的两处单例模式用的还是不同的方式实现的,那么单例模式到底有几...

  • 单例模式,你真的写对了吗?

    看公司代码的时候发现项目中单例模式应用挺多的,并且发现的两处单例模式用的还是不同的方式实现的,那么单例模式到底有几...

  • Python经典面试题21道

    1、Python如何实现单例模式? Python有两种方式可以实现单例模式,下面两个例子使用了不同的方式实现单例模...

  • Python最经典面试题21道,必看!

    1、Python如何实现单例模式? Python有两种方式可以实现单例模式,下面两个例子使用了不同的方式实现单例模...

  • Python 经典面试题

    1、Python如何实现单例模式? Python有两种方式可以实现单例模式,下面两个例子使用了不同的方式实现单例模...

网友评论

    本文标题:【TS】另一种实现typescript单例模式的方式(支持代码提

    本文链接:https://www.haomeiwen.com/subject/frqxwdtx.html