混入
除了传统的 OO 层次结构之外,另一种从可重用组件构建类的流行方法是通过组合更简单的部分类来构建它们。您可能熟悉 Scala 等语言的 mixin 或特征的概念,并且该模式在 JavaScript 社区中也颇为流行。
混入是如何工作的?
该模式依赖于使用具有类继承的泛型来扩展基类。TypeScript 最好的 mixin 支持是通过类表达式模式完成的。您可以阅读有关此模式在 JavaScript 此处 中如何工作的更多信息。
开始之前,我们需要一个类,它会将 mixins 应用在:
class Sprite {
name = "";
x = 0;
y = 0;
constructor(name: string) {
this.name = name;
}
}
然后你需要一个类型和一个工厂函数,它返回一个扩展基类的类表达式。
// 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;
}
};
}
完成这些设置后,您可以创建一个类,该类代表应用了 mixins 的基类:
class Sprite {
name = "";
x = 0;
y = 0;
constructor(name: string) {
this.name = name;
}
}
type Constructor = new (...args: any[]) => {};
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;
}
};
}
// 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);
受约束的混入
qw/Gn+oAXBvKNMBUAfMRt/bhu86/6Cp/ggFaVqi0OQAnBX+fumTPVdOBLE61ZgxLEAvOoT6FsQ5LSxrWEch7AKIjt4S8dsGCReoJp5jUqI1s+K+lFScLkqINDVYm/YrPav6eiOTPM2R/EGexgN5dLA==
Eg8QMVYkXb0grmOd1JTtFjIDVheKkokLrypf3uA9MhKric2e/bq6rw5OJWEcAvQ+c8rdwrG13qIVbncUvQ7/SpqMV3nnNnBltru1A+GSQCMGM3+kgkq7MSnnbmeurAd2
// 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;
hhRA/ZFidq/SkYujmfGmFpJSdMeUb9sbBEZxE8hz0gtb+/BONIPOM7Kj90hx804EIRHotmvp7cnt7wwFf5b0Jw==
type GConstructor<T = {}> = new (...args: any[]) => T;
class Sprite {
name = "";
x = 0;
y = 0;
constructor(name: string) {
this.name = name;
}
}
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<Sprite>;
type Loggable = GConstructor<{ print: () => void }>;
A9aSasb7OVGbTZ9dqXlm1XVDEDu78av3UZi10tzASzGNaVEyTaTzWHNbQUXf0GYhEULPUnWFYF4dooKKzAeZ14qY0KsFJ0ervVK8xFnM7xURW8AuD6S3gNQnPz+Kui/B
type GConstructor<T = {}> = new (...args: any[]) => T;
class Sprite {
name = "";
x = 0;
y = 0;
constructor(name: string) {
this.name = name;
}
}
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<Sprite>;
type Loggable = GConstructor<{ print: () => void }>;
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);
}
};
}
替代模式
gBsYSTnUYCVeR40kSSETjB4/GDwlR6Q2wZFhCOszLaddJOJ/Az1Us4y7nZPo8NqL8xOCNGbTdWQAv32dwk3lUzeYqKPVKjsIOkyQk23SUdpQ+2KvH5nHh2NcxlfjqqSyHmdibgBU8vxfDpNhFMEhg2G331C6mZRh2PvY+4Jo2VZU7T1Brr/Z2gd9fBBv1MRCB+DP8eynfe4FFPZluOoFlg==
// 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)
);
});
});
}
lJEYnB76t6a4er/nhtPvYwn4n9UWCOgtbsGMn7OpKIZvoy9TgbD3rk5zOvKY4Sh+q+q5CNRmm5MsH0onJvjdeu5y/kLS9PhNNgP8xytoQ0SkFagDmHJ+y6p0breJTV506E1BtBQIp16ZscBZ/Fumb+OHSpuSvT5iWvSzynbOR0/AfLC0AmN4aRpirTLkckHv
约束条件
djnbbns8Mvm/Hfrj3t+zwdRqXQ57Lh+8fED0EslvgPu5nrh32hmazjkAmdMU97qP82OiDaf5p3U0DTtmwbL+rKC6BSHVCsoxPCiyZi4xl/WPAYKPkFGnR2gCU/aFcy8jeGeb7d/EdCuo5IRAfyzRfta3Z33h1eIM8XUWGLL9mt2+/9vDrI/IZIUBoz4VLcGH
装饰器和混入 #4881
VY/BOjpJSby/2PAejJ4lBU9PvhSRIVAaRrsEO/hWsVmygbHNEG4IFpNqc+HlQ7Zh/IDiOzcdVJBjU8yaCuIpuQ==
// 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;
// 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;
静态属性混入 #17829
l0epS8QsjAGM1EEv7zNO60Jed9SZPIKwp7isIw3Lte/IvgWYi8jD3VWx4SYOJhQ2XydAfdsYz2wgNm1j9IxjfU5E1vffvK6uNUs4E9778VfT7hKkDEQ0v4rdeMY7gRyLpm3sP4VebSkAEvpEGW0LAW9zpo4O1zwX2rD7UErYRCdJRDX9YnBQw1E0bHl9Ewlp0ZTOPXN4wpymmJmqYxJ5MQ==
FV+7ddxy6OfaXfQfVmH3nTfBmgCqL9vJrAdXaKSJhF2O8fVUy6lr8u6/OlHv6kJKS8+MbTmA4fT2iVnHXF7oHfy77fkbMW0PKyqe0T4W7j0=
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