除了傳統的物件導向層級結構之外,另一種從可重複使用的元件建構類別的熱門方式,是透過結合更簡單的部分類別來建構。你可能熟悉 Scala 等語言的 mixin 或特質概念,而此模式在 JavaScript 社群中也頗受歡迎。
Mixin 如何運作?
此模式仰賴使用具有類別繼承的泛型來擴充基礎類別。TypeScript 最佳的 mixin 支援是透過類別表達式模式完成。您可以在 此處進一步了解此模式在 JavaScript 中的運作方式。
首先,我們需要一個類別,將 mixin 套用在其上
tsTry
classSprite {name = "";x = 0;y = 0;constructor(name : string) {this.name =name ;}}
然後您需要一個類型和一個工廠函式,傳回一個擴充基礎類別的類別表達式。
tsTry
// To get started, we need a type which we'll use to extend// other classes from. The main responsibility is to declare// that the type being passed in is a class.typeConstructor = new (...args : any[]) => {};// This mixin adds a scale property, with getters and setters// for changing it with an encapsulated private property:functionScale <TBase extendsConstructor >(Base :TBase ) {return classScaling extendsBase {// Mixins may not declare private/protected properties// however, you can use ES2020 private fields_scale = 1;setScale (scale : number) {this._scale =scale ;}getscale (): number {return this._scale ;}};}
設定好這些之後,您就可以建立一個類別,表示已套用 mixin 的基礎類別
tsTry
// Compose a new class from the Sprite class,// with the Mixin Scale applier:constEightBitSprite =Scale (Sprite );constflappySprite = newEightBitSprite ("Bird");flappySprite .setScale (0.8);console .log (flappySprite .scale );
受限 Mixin
在上述形式中,mixin 沒有基礎類別的底層知識,這可能會讓您難以建立想要的設計。
為了建模這個,我們修改原始建構函式類型以接受一個泛型引數。
tsTry
// This was our previous constructor:typeConstructor = new (...args : any[]) => {};// Now we use a generic version which can apply a constraint on// the class which this mixin is applied totypeGConstructor <T = {}> = new (...args : any[]) =>T ;
這允許建立僅適用於受限基礎類別的類別
tsTry
typePositionable =GConstructor <{setPos : (x : number,y : number) => void }>;typeSpritable =GConstructor <Sprite >;typeLoggable =GConstructor <{
然後你可以建立混入,它只在你有一個特定基礎可以建構時才會運作
tsTry
functionJumpable <TBase extendsPositionable >(Base :TBase ) {return classJumpable extendsBase {jump () {// This mixin will only work if it is passed a base// class which has setPos defined because of the// Positionable constraint.this.setPos (0, 20);}};}
替代模式
此文件的前幾個版本建議一種撰寫混入的方式,其中你分別建立執行時期和類型階層,然後在最後合併它們
tsTry
// Each mixin is a traditional ES classclassJumpable {jump () {}}classDuckable {duck () {}}// Including the baseclassSprite {x = 0;y = 0;}// Then you create an interface which merges// the expected mixins with the same name as your baseinterfaceSprite extendsJumpable ,Duckable {}// Apply the mixins into the base class via// the JS at runtimeapplyMixins (Sprite , [Jumpable ,Duckable ]);letplayer = newSprite ();player .jump ();console .log (player .x ,player .y );// This can live anywhere in your codebase:functionapplyMixins (derivedCtor : any,constructors : any[]) {constructors .forEach ((baseCtor ) => {Object .getOwnPropertyNames (baseCtor .prototype ).forEach ((name ) => {Object .defineProperty (derivedCtor .prototype ,name ,Object .getOwnPropertyDescriptor (baseCtor .prototype ,name ) ||Object .create (null));});});}
此模式較不依賴編譯器,而較依賴你的程式碼庫,以確保執行時期和類型系統都正確地保持同步。
限制
混入模式由 TypeScript 編譯器透過程式碼流程分析原生支援。有幾個情況你可能會遇到原生支援的邊緣。
裝飾器和 Mixin #4881
您無法使用裝飾器透過程式碼流程分析提供 Mixin
tsTry
// A decorator function which replicates the mixin pattern:constPausable = (target : typeofPlayer ) => {return classPausable extendstarget {shouldFreeze = false;};};@Pausable classPlayer {x = 0;y = 0;}// The Player class does not have the decorator's type merged:constplayer = newPlayer ();Property 'shouldFreeze' does not exist on type 'Player'.2339Property 'shouldFreeze' does not exist on type 'Player'.player .; shouldFreeze // The runtime aspect could be manually replicated via// type composition or interface merging.typeFreezablePlayer =Player & {shouldFreeze : boolean };constplayerTwo = (newPlayer () as unknown) asFreezablePlayer ;playerTwo .shouldFreeze ;
靜態屬性 Mixin #17829
這比較像是陷阱而不是限制。類別表達式模式會建立單例,因此無法在類型系統中對應到支援不同變數類型。
您可以使用函式來解決這個問題,以傳回根據泛型而有所不同的類別
tsTry
functionbase <T >() {classBase {staticprop :T ;}returnBase ;}functionderived <T >() {classDerived extendsbase <T >() {staticanotherProp :T ;}returnDerived ;}classSpec extendsderived <string>() {}Spec .prop ; // stringSpec .anotherProp ; // string