首先,在 js 中還沒有真正的多繼承。但是在實際工作中經常需要抽離通用模組並按需組成新的業務模組,這就對類的多繼承有了實際需求。
舉個例子,現在我們有個基礎類 Animal
:
class Animal {
constructor(name?: string) {
if (name) this.myName = name;
}
myName: string = "animal";
}
另外有兩個 Animal
的子類,分別是會跑的 Horse
和會飛的 Bird
:
class Horse extends Animal {
constructor(...arg: any[]) {
super(...arg);
// ...do something
}
run() {
console.log("I can run");
}
}
class Bird extends Animal {
@Decorator() // 帶有裝飾器
fly() {
console.log("I can fly");
}
static wings = 2;
}
function Decorator(): MethodDecorator {
return function (target, propKey, descriptor) {
console.log(`${target.constructor.name} ${String(propKey)}`);
};
}
現在我們需要一個同時繼承 Horse
和 Bird
特性的既會跑又會飛的 Unicorn
,重新寫新的類既不高效也不優雅。下面我們來討論如何實現多繼承。
為了便於後續說明,先定義幾個型別宣告:
type Constructor<T = unknown> = new (...args: any[]) => T;
type ConstructorExtFn<T extends Constructor> = (Cls: T) => T;
type UnionToIntersection<T> = UnionToFunction<T> extends (arg: infer P) => any ? P : never;
type UnionToFunction<T> = T extends any ? (arg: T) => any : never;
傳統 mixin
這種方式原理類似於 Object.assign
,將多個類的原型方法、屬性、靜態屬性等複製到目標類上,以下是簡單實現:
function Mixin<T extends Constructor<{}>[]>(
...mixins: T
): Constructor<UnionToIntersection<InstanceType<T[number]>>> & UnionToIntersection<T[number]> {
class MixinBase {}
function copyProperties(target: any, source: any) {
for (const key of Reflect.ownKeys(source)) {
const skipProps = ["constructor", "prototype", "name"];
if (!skipProps.includes(String(key))) {
const desc = Object.getOwnPropertyDescriptor(source, key);
if (desc) Object.defineProperty(target, key, desc);
}
}
}
for (const mixin of mixins) {
copyProperties(MixinBase, mixin);
copyProperties(MixinBase.prototype, mixin.prototype);
}
return MixinBase as any;
}
Animal.prototype.myName = "animal"; // 需在原型鏈設定預設值,否則下方結果為 undefined
class Unicorn extends Mixin(Animal, Horse, Bird) {
constructor() {
super("unicorn");
}
}
console.log(Unicorn.wings); // 2
const unicorn = new Unicorn();
console.log(unicorn.myName); // animal
unicorn.run(); // I can run
unicorn.fly(); // I can fly
unicorn.speak(); // error
優點:
- 返回的類的原型鏈乾淨,便於追溯
缺點:
- 需要額外處理構造器、靜態屬性、同名方法,完備實現較為複雜
- 需要宣告返回型別
- 屬性預設值需額外定義在原型鏈上
- 不支援 super
- 無法繼承父類中的裝飾器
使用子類工廠函式
下面介紹的方法將使用子類工廠函式來實現多繼承。該方法接受一個基類作為引數,返回繼承這個基類的子類,具體邏輯在該子類中實現。下面將重寫上述需求:
function mixinHorse<T extends Constructor<Animal>>(Cls: T) {
class Horse extends Cls {
constructor(...arg: any[]) {
super(...arg);
// ...do something
}
run() {
console.log("I can run");
}
}
return Horse;
}
const Horse = mixinHorse(Animal);
function mixinBird<T extends Constructor<Animal>>(Cls: T) {
class Bird extends Cls {
@Decorator() // 帶有裝飾器
fly() {
console.log("I can fly");
}
static wings = 2;
}
return Bird;
}
const Bird = mixinBird(Animal);
class Unicorn extends mixinBird(mixinHorse(Animal)) {
constructor() {
super("unicorn");
}
}
console.log(Unicorn.wings); // 2
const unicorn = new Unicorn();
console.log(unicorn.myName); // unicorn
unicorn.run(); // I can run
unicorn.fly(); // I can fly
unicorn.speak(); // error
可以看到,只需改變寫法而無需額外程式碼就能完備實現多繼承功能。 super
和裝飾器功能也正常工作
優點:
- 實現簡單,執行結果符合預期
- 自帶型別推導
- 方便重寫同名方法
- super 和裝飾器正常工作
缺點:
- 原型鏈較長,繼承過多會導致原型鏈過於複雜
- 需改寫子類工廠函式,不便於直接使用中間類
- 巢狀寫法導致繼承過多時不便閱讀,類似回撥地獄
最佳化寫法
我們先來看下繼承過多的情況:
class NewAnimal extends Mixin5(Mixin4(Mixin3(mixinBird(mixinHorse(Animal))))) {
// ...do something
}
這裡的多層巢狀像極了 js 中的回撥地獄,既不方便閱讀,也不方便增減。對於這個問題,我們可以最佳化寫法使使用更加方便。程式碼如下:
export function multiExtends(
extendsBase: Constructor<any>,
extendsFunctions: ConstructorExtFn<Constructor<any>>[]
) {
let func: ConstructorExtFn<Constructor<any>>;
let ans: Constructor<any> = extendsBase;
while (extendsFunctions.length) {
func = extendsFunctions.shift()!;
ans = func(ans);
}
return ans;
}
上面 multiExtends
函式接受一個基類 extendsBase
和一個子類工廠函式陣列 extendsFunctions
,返回最終繼承結果。但是這樣就丟失了 ts 的自動型別推導,需要自己修改返回型別:
export function multiExtends<
B extends Constructor<any>,
E extends ConstructorExtFn<Constructor<any>>
>(
extendsBase: B,
extendsFunctions: E[]
): B & UnionToIntersection<ReturnType<E>> {
let func: ConstructorExtFn<Constructor<any>>;
let ans: Constructor<any> = extendsBase;
while (extendsFunctions.length) {
func = extendsFunctions.shift()!;
ans = func(ans);
}
return ans as any;
}
返回型別重點是將聯合型別
E
轉為交叉型別
class Unicorn extends multiExtends(Animal, [
mixinHorse,
mixinBird,
Mixin3,
Mixin4,
Mixin5,
]) {
constructor() {
super("unicorn");
}
// ...do something
}
console.log(Unicorn.wings); // 2
const unicorn = new Unicorn();
console.log(unicorn.myName); // unicorn
unicorn.run(); // I can run
unicorn.fly(); // I can fly
unicorn.speak(); // error
可以看到寫法簡介優雅了不少,且功能完備。
上述功能筆者已整理併發布了 npm 包,以便使用:
npm i multi-extends
import { multiExtends } from "multi-extends";
// ...