美文网首页js css html
【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 成员不可访问

    相关文章

      网友评论

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

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