除了傳統的面向物件層級結構之外,透過組合簡單的部分類(partial classes)來構建可重用的元件也是一種流行的方法。你可能熟悉 Scala 等語言中的 Mixins(混入)或 Traits(特徵)概念,這種模式在 JavaScript 社群中也頗受歡迎。
Mixins 是如何工作的?
這種模式依賴於將泛型與類繼承結合使用來擴充套件基類。TypeScript 對 Mixins 最好的支援是透過類表示式(class expression)模式實現的。你可以在這裡閱讀更多關於此模式在 JavaScript 中如何工作的內容。
首先,我們需要一個將被應用 Mixins 的基礎類。
tsTryclassSprite {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 ;}};}
準備好這些之後,你就可以建立一個類,它代表了應用了 Mixins 的基類。
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 );
受約束的 Mixins (Constrained Mixins)
在上面的形式中,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 ;
這允許建立僅適用於受約束基類的類。
tsTrytypePositionable =GConstructor <{setPos : (x : number,y : number) => void }>;typeSpritable =GConstructor <Sprite >;typeLoggable =GConstructor <{
然後,你可以建立僅在擁有特定構建基類時才生效的 Mixins。
tsTryfunctionJumpable <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);}};}
替代模式
該文件的早期版本推薦了一種編寫 Mixins 的方式:分別建立執行時和型別層級,最後再將它們合併。
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));});});}
這種模式對編譯器的依賴較少,而更多地依賴於你的程式碼庫來確保執行時和型別系統保持同步。
限制
Mixin 模式透過程式碼流分析在 TypeScript 編譯器中得到原生支援。在極少數情況下,你可能會遇到原生支援的邊緣情況。
裝飾器與 Mixins #4881
你不能使用裝飾器透過程式碼流分析來提供 Mixins。
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 ;
靜態屬性 Mixins #17829
這與其說是一個限制,不如說是一個“陷阱”。類表示式模式建立的是單例,因此它們無法在型別系統中進行對映以支援不同的變數型別。
你可以透過使用函式來返回基於泛型而不同的類來繞過這個問題。
tsTryfunctionbase <T >() {classBase {staticprop :T ;}returnBase ;}functionderived <T >() {classDerived extendsbase <T >() {staticanotherProp :T ;}returnDerived ;}classSpec extendsderived <string>() {}Spec .prop ; // stringSpec .anotherProp ; // string