我之前寫過一個(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)的幫助,我們保留了代碼提示的功能與單例模式基類不同的是呛伴,本文的方式通過函數(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ù)使用
protected
或private
訪問限定符售貌; - 使用
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
定義之前使用SampleClass
為undefined
兰怠。如果用let
或者const
定義就不會(huì)有變量提升的問題梦鉴,會(huì)直接報(bào)錯(cuò):error TS2448: Block-scoped variable 'SampleClass' used before its declaration.
。這里我更建議使用const
揭保; - IDE有可能無法實(shí)時(shí)提示private 成員不可訪問和protected 成員不可訪問肥橙;