混入

除了傳統的物件導向層級結構之外,另一種從可重複使用的元件建構類別的熱門方式,是透過結合更簡單的部分類別來建構。你可能熟悉 Scala 等語言的 mixin 或特質概念,而此模式在 JavaScript 社群中也頗受歡迎。

Mixin 如何運作?

此模式仰賴使用具有類別繼承的泛型來擴充基礎類別。TypeScript 最佳的 mixin 支援是透過類別表達式模式完成。您可以在 此處進一步了解此模式在 JavaScript 中的運作方式。

首先,我們需要一個類別,將 mixin 套用在其上

ts
class Sprite {
name = "";
x = 0;
y = 0;
 
constructor(name: string) {
this.name = name;
}
}
Try

然後您需要一個類型和一個工廠函式,傳回一個擴充基礎類別的類別表達式。

ts
// 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.
 
type Constructor = new (...args: any[]) => {};
 
// This mixin adds a scale property, with getters and setters
// for changing it with an encapsulated private property:
 
function Scale<TBase extends Constructor>(Base: TBase) {
return class Scaling extends Base {
// Mixins may not declare private/protected properties
// however, you can use ES2020 private fields
_scale = 1;
 
setScale(scale: number) {
this._scale = scale;
}
 
get scale(): number {
return this._scale;
}
};
}
Try

設定好這些之後,您就可以建立一個類別,表示已套用 mixin 的基礎類別

ts
// Compose a new class from the Sprite class,
// with the Mixin Scale applier:
const EightBitSprite = Scale(Sprite);
 
const flappySprite = new EightBitSprite("Bird");
flappySprite.setScale(0.8);
console.log(flappySprite.scale);
Try

受限 Mixin

在上述形式中,mixin 沒有基礎類別的底層知識,這可能會讓您難以建立想要的設計。

為了建模這個,我們修改原始建構函式類型以接受一個泛型引數。

ts
// This was our previous constructor:
type Constructor = new (...args: any[]) => {};
// Now we use a generic version which can apply a constraint on
// the class which this mixin is applied to
type GConstructor<T = {}> = new (...args: any[]) => T;
Try

這允許建立僅適用於受限基礎類別的類別

ts
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<Sprite>;
type Loggable = GConstructor<{ print: () => void }>;
Try

然後你可以建立混入,它只在你有一個特定基礎可以建構時才會運作

ts
function Jumpable<TBase extends Positionable>(Base: TBase) {
return class Jumpable extends Base {
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);
}
};
}
Try

替代模式

此文件的前幾個版本建議一種撰寫混入的方式,其中你分別建立執行時期和類型階層,然後在最後合併它們

ts
// Each mixin is a traditional ES class
class Jumpable {
jump() {}
}
 
class Duckable {
duck() {}
}
 
// Including the base
class Sprite {
x = 0;
y = 0;
}
 
// Then you create an interface which merges
// the expected mixins with the same name as your base
interface Sprite extends Jumpable, Duckable {}
// Apply the mixins into the base class via
// the JS at runtime
applyMixins(Sprite, [Jumpable, Duckable]);
 
let player = new Sprite();
player.jump();
console.log(player.x, player.y);
 
// This can live anywhere in your codebase:
function applyMixins(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)
);
});
});
}
Try

此模式較不依賴編譯器,而較依賴你的程式碼庫,以確保執行時期和類型系統都正確地保持同步。

限制

混入模式由 TypeScript 編譯器透過程式碼流程分析原生支援。有幾個情況你可能會遇到原生支援的邊緣。

裝飾器和 Mixin #4881

您無法使用裝飾器透過程式碼流程分析提供 Mixin

ts
// A decorator function which replicates the mixin pattern:
const Pausable = (target: typeof Player) => {
return class Pausable extends target {
shouldFreeze = false;
};
};
 
@Pausable
class Player {
x = 0;
y = 0;
}
 
// The Player class does not have the decorator's type merged:
const player = new Player();
player.shouldFreeze;
Property 'shouldFreeze' does not exist on type 'Player'.2339Property 'shouldFreeze' does not exist on type 'Player'.
 
// The runtime aspect could be manually replicated via
// type composition or interface merging.
type FreezablePlayer = Player & { shouldFreeze: boolean };
 
const playerTwo = (new Player() as unknown) as FreezablePlayer;
playerTwo.shouldFreeze;
Try

靜態屬性 Mixin #17829

這比較像是陷阱而不是限制。類別表達式模式會建立單例,因此無法在類型系統中對應到支援不同變數類型。

您可以使用函式來解決這個問題,以傳回根據泛型而有所不同的類別

ts
function base<T>() {
class Base {
static prop: T;
}
return Base;
}
 
function derived<T>() {
class Derived extends base<T>() {
static anotherProp: T;
}
return Derived;
}
 
class Spec extends derived<string>() {}
 
Spec.prop; // string
Spec.anotherProp; // string
Try

TypeScript 文件是一個開放原始碼專案。協助我們改善這些頁面 傳送 Pull Request

此頁面的貢獻者
OTOrta Therox (16)
GMGleb Maksimenko (1)
IOIván Ovejero (1)
DEDom Eccleston (1)
OOblosys (1)
5+

最後更新:2024 年 3 月 21 日