【TS】另一種實(shí)現(xiàn)typescript單例模式的方式(支持代碼提示打掘,禁止二次實(shí)例化)

我之前寫過一個(gè)ts單例模式的基類(傳從門:實(shí)現(xiàn)一個(gè)ts單例模式基類(支持代碼提示、禁止二次實(shí)例化) - 簡書 (jianshu.com))甲脏。但是經(jīng)過我思考以后拓诸,覺得還有另一種方式創(chuàng)建通用的單例模式。
那么先上代碼:

/**
 * 單例類的創(chuàng)建器
 * @param cls 需要單例化的類
 * @example const AClass = singleton(class { ... });
 */
function singleton<T extends { new(...args: any[]): {}, prototype: any }>(cls: T): T & { instance: T["prototype"] } {
    // 實(shí)例
    let instance: any = null;
    // 構(gòu)造函數(shù)代理
    let constructorProxy: ProxyConstructor | null = null;
    const proxy = new Proxy(
        cls,
        {
            construct(target: any, argArray: any[], newTarget: any): T {
                if (!instance) {
                    instance = new cls(...argArray);
                    // 下面這一行用于替換掉construct函數(shù)桑李,減少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()創(chuàng)建新對(duì)象
                    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的類型轉(zhuǎn)換成函數(shù)簽名的返回類型(T & { instance: T["prototype"] }),不然在某些環(huán)境中可能會(huì)出現(xiàn)錯(cuò)誤
}

由于我們的singleton不是類贵白,而是普通的函數(shù)率拒,我們?cè)谑褂玫臅r(shí)候就需要傳入一個(gè)類,并且用一個(gè)變量接收返回值禁荒。
示例代碼:

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

控制臺(tái)打逾颉:

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類型系統(tǒng)的幫助,我們保留了代碼提示的功能
成員屬性提示
靜態(tài)屬性提示

與單例模式基類不同的是呛伴,本文的方式通過函數(shù)調(diào)用返回一個(gè)代理對(duì)象(Proxy)勃痴。利用Proxy我們可以阻止外部直接訪問類。
Proxy的第二個(gè)參數(shù)對(duì)象中可以編寫construct陷阱函數(shù)热康,用于攔截new操作符沛申,下面是construct的函數(shù)簽名:

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;

    // 省略了其他的定義
}

當(dāng)代理攔截到企圖利用new創(chuàng)建新對(duì)象時(shí),如果是第一次實(shí)例化姐军,那么允許創(chuàng)建對(duì)象铁材;反之返回之前創(chuàng)建的對(duì)象。這樣可以防止多次實(shí)例化:

construct(target: any, argArray: any[], newTarget: any): T {
    if (!instance) {
        instance = new cls(...argArray);
        // 下面這一行用于替換掉construct函數(shù)奕锌,減少instance判斷衫贬,也可以刪去這行代碼
        this.construct = (target: any, argArray: any[], newTarget: any): T => instance;
    }
    return instance;
},

為了支持SampleClass.instance方式獲取實(shí)例,我們可以在get陷阱函數(shù)中返回instance對(duì)象歇攻。我這里直接使用了new proxy()固惯,讓construct代替我們返回instance對(duì)象:

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

同時(shí)在set函數(shù)中阻止對(duì)instance賦值

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

以上做法還是不足以完全攔截多次實(shí)例化,通過new (SampleClass.prototype.constructor as any)()還是可以再次創(chuàng)建新對(duì)象缴守。那么我們還需要對(duì)SampleClass.prototype.constructor進(jìn)行代理葬毫。做法是將前面提到的get陷阱函數(shù)改成以下代碼:

get(target: T, p: string | symbol, receiver: any): any {
    if (p === "instance") {
        return new proxy();
    }
    if (p === "prototype") {
        // 用于阻止通過new SampleClass.prototype.constructor()創(chuàng)建新對(duì)象
        // 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];
}

寫完了邏輯相關(guān)的代碼贴捡,我們?cè)賮韺扅c(diǎn)類型相關(guān)的代碼忽肛。

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

對(duì)于上面這個(gè)函數(shù)簽名,<T extends { new(...args: any[]): {}, prototype: any }>(cls: T)表示需要傳入的參數(shù)需要有構(gòu)造函數(shù)和原型屬性烂斋,也就是一個(gè)類屹逛,且不限制構(gòu)造函數(shù)的參數(shù)個(gè)數(shù)和類型。函數(shù)的返回值類型首先需要返回cls類的類型汛骂,也就是T罕模,但是這樣ts類型系統(tǒng)無法知道里面有instance屬性,所以這里需要改成交叉類型帘瞭,而且instance的類型需要為cls類的原型淑掌,結(jié)果就是T & { instance: T["prototype"] }。簡單來說蝶念,T表示了類中有哪些靜態(tài)屬性抛腕,而T["prototype"]表示類中有哪些成員屬性。

以上的方法有以下優(yōu)缺點(diǎn):

優(yōu)點(diǎn):

  • 保留了代碼提示媒殉;
  • 依然可以使用new SampleClass()担敌,只不過會(huì)得到之前創(chuàng)建過的實(shí)例;
  • 可以直接使用SampleClass.instance屬性獲取實(shí)例廷蓉,而不一定得使用SampleClass.getInstance()方法全封;
  • 保留了類唯一一次寶貴的繼承機(jī)會(huì),不用因?yàn)槔^承單例模式基類而無法繼承其他類苦酱;

缺點(diǎn):

  • 無法再對(duì)構(gòu)造函數(shù)使用protectedprivate訪問限定符售貌;
  • 使用SampleClass.instance的方式獲取實(shí)例時(shí)無法對(duì)構(gòu)造函數(shù)進(jìn)行傳參给猾,但是通過new操作符可以在第一次實(shí)例化的時(shí)候傳參疫萤,有可能導(dǎo)致意想不到的問題,建議不要使用構(gòu)造函數(shù)參數(shù)敢伸;
  • 使用const SampleClass = singleton(class { ... });創(chuàng)建類的方式不太常用扯饶,比較奇怪;
  • IDE不再主動(dòng)將SampleClass當(dāng)成一個(gè)類了池颈,它的類型和在編輯器中的樣式將有別于普通的類尾序;
    單例化的類
    普通的類
  • 無法在同一行中使用默認(rèn)導(dǎo)出了,需要另起一行進(jìn)行默認(rèn)導(dǎo)出躯砰,影響不大每币;
  • 如果使用var的方式定義SampleClass變量,會(huì)產(chǎn)生變量提升的問題琢歇,在var定義之前使用SampleClassundefined兰怠。如果用let或者const定義就不會(huì)有變量提升的問題梦鉴,會(huì)直接報(bào)錯(cuò):error TS2448: Block-scoped variable 'SampleClass' used before its declaration.。這里我更建議使用const揭保;
  • IDE有可能無法實(shí)時(shí)提示private 成員不可訪問protected 成員不可訪問肥橙;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市秸侣,隨后出現(xiàn)的幾起案子存筏,更是在濱河造成了極大的恐慌,老刑警劉巖味榛,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件椭坚,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡励负,警方通過查閱死者的電腦和手機(jī)藕溅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來继榆,“玉大人巾表,你說我怎么就攤上這事÷远郑” “怎么了集币?”我有些...
    開封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長翠忠。 經(jīng)常有香客問我鞠苟,道長,這世上最難降的妖魔是什么秽之? 我笑而不...
    開封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任当娱,我火速辦了婚禮,結(jié)果婚禮上考榨,老公的妹妹穿的比我還像新娘跨细。我一直安慰自己,他們只是感情好河质,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開白布冀惭。 她就那樣靜靜地躺著,像睡著了一般掀鹅。 火紅的嫁衣襯著肌膚如雪散休。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天乐尊,我揣著相機(jī)與錄音戚丸,去河邊找鬼。 笑死扔嵌,一個(gè)胖子當(dāng)著我的面吹牛限府,可吹牛的內(nèi)容都是我干的猴鲫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼谣殊,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼拂共!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起姻几,我...
    開封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤宜狐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后抚恒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡络拌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年俭驮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片春贸。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡混萝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出萍恕,到底是詐尸還是另有隱情逸嘀,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布允粤,位于F島的核電站崭倘,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏类垫。R本人自食惡果不足惜司光,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望悉患。 院中可真熱鬧残家,春花似錦、人聲如沸购撼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽迂求。三九已至,卻和暖如春晃跺,著一層夾襖步出監(jiān)牢的瞬間揩局,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來泰國打工掀虎, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留凌盯,地道東北人付枫。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像驰怎,于是被迫代替她去往敵國和親阐滩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容