js css htmlTypeScript基础

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

2023-11-07  本文已影响0人  来一斤BUG

我之前写过一个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"]表示类中有哪些成员属性。

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

优点:

缺点:

上一篇 下一篇

猜你喜欢

热点阅读