首先,在 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)}`);
};
}

现在我们需要一个同时继承 HorseBird 特性的既会跑又会飞的 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";

// ...

如何TypeScript中相对优雅地实现类的多继承的更多相关文章

  1. Typescript中的可索引接口 类类型接口

    /* 5.typeScript中的接口 可索引接口 类类型接口 */ /* 接口的作用:在面向对象的编程中,接口是一种规范的定义,它定义了行为和动作的规范,在程序设计里面,接口起到一种限制和规范的作用 ...

  2. 【TypeScript】如何在TypeScript中使用async/await,让你的代码更像C#。

    [TypeScript]如何在TypeScript中使用async/await,让你的代码更像C#. async/await 提到这个东西,大家应该都很熟悉.最出名的可能就是C#中的,但也有其它语言也 ...

  3. TypeScript中的怪语法

    TypeScript中的怪语法 如何处理undefined 和 null undefined的含义是:一个变量没有初始化. null的含义是:一个变量的值是空. undefined 和 null 的最 ...

  4. JavaScript 和 TypeScript 中的 class

    对于一个前端开发者来说,很少用到 class ,因为在 JavaScript 中更多的是 函数式 编程,抬手就是一个 function,几乎不见 class 或 new 的踪影.所以 设计模式 也是大 ...

  5. typescript中的接口

    说到接口:在面向对象的编程中,接口是一种规范的定义,它定义了行为和动作的规范,在程序设计里面,接口起到一种限制和规范的作用.接口定义了某一批类所需要遵守的规范,接口不关心这些类的内部状态数据,也不关心 ...

  6. Typescript中的装饰器原理

    Typescript中的装饰器原理 1.小原理 因为react中的高阶组件本质上是个高阶函数的调用, 所以高阶组件的使用,我们既可以使用函数式方法调用,也可以使用装饰器. 也就是说,装饰器的本质就是一 ...

  7. React中如何优雅的捕捉事件错误

    React中如何优雅的捕捉事件错误 前话 人无完人,所以代码总会出错,出错并不可怕,关键是怎么处理. 我就想问问大家react的错误怎么捕捉呢? 这个时候: 小白:怎么处理? 小白+: ErrorBo ...

  8. Kotlin(2): 优雅地扩展类的方法和属性

    欢迎Follow我的GitHub, 关注我的CSDN. 个人博客: http://www.wangchenlong.org/, 最新内容. Kotlin由JetBrains公司推出, 是兼容Java的 ...

  9. Python项目中如何优雅的import

    Python项目中如何优雅的import 前言 之前有一篇关于Python编码规范的随笔, 但是写的比较杂乱, 因为提到了import语句, 在篇文章中, 我专门来讲Python项目中如何更好的imp ...

  10. 十分钟教你理解TypeScript中的泛型

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者.原文出处:https://blog.bitsrc.io/understanding-generics-in-t ...

随机推荐

  1. TS2Vec: 面向通用的时间序列表示《TS2Vec: Towards Universal Representation of Time Series》(时间序列、对比学习、多尺度特征(池化操作)、分层对比、上下文一致性(时间戳掩码+随机裁剪))

    今天是2024年5月22日,10:24,今天看这篇经典的论文(如果你问我为什么最近频繁看论文,因为我的创新点无了,要找创新点+太菜了,菜就多看多学). 论文:TS2Vec: Towards Unive ...

  2. CSS & JS Effect – Textarea Autoresize

    前言 这是一个很普遍的体验, 而且实现起来也很简单哦 参考 YouTube – How to Auto Resize Textarea using HTML CSS & JavaScript ...

  3. JavaScript – 基本语法

    参考 阮一峰 – 基本语法 Switch switch 经常用来取代 else if, 因为可读性比价高, 而且通常性能也比较好. standard 长这样 const orderStatus = ' ...

  4. Figma 学习笔记 – Color

    大纲 Figma 的颜色是通过 FIll 实现的 (Fill 还有其它功能比如 fill 图片) 整体大概长这样, 我们一个一个看 颜色和 opacity

  5. JavaScript——事件监听

    事件监听      1.事件绑定        2.常见事件     

  6. 以后基于 Topass 的博客加密方法通告

    Topass 加密方法 以后会将部分未公开内容公开,请你通过此加密途径来破解密码 特别地,为了保证博客的浏览体验,我不会通过这种方法加密任何一种应该公开的文章 话说你们不妨猜猜用的什么算法

  7. YAML 文件基本语法格式(十四)

    一.YAML 文件基本语法格式 前面我们得 Kubernetes 集群已经搭建成功了,现在我们就可以在集群里面来跑我们的应用了.要在集群里面运行我们自己的应用,首先我们需要知道几个概念. 第一个当然就 ...

  8. 深入理解Linux进程调度(下)

    一.SMP管理 在继续讲解之前,我们先来说一下多CPU管理(这里的CPU是指逻辑CPU,在很多语境中CPU都是默认指的逻辑CPU,物理CPU要特别强调是物理CPU).最开始的时候计算机都是单CPU的, ...

  9. USB 逻辑分析仪分析丢包怎么分析(lecroy USB 逻辑分析仪)

    使用 LeCroy USB 逻辑分析仪分析 USB 数据传输中的丢包现象,通常涉及以下步骤: 1. 设置触发条件 在 LeCroy USB 逻辑分析仪中,设置适当的触发条件来捕获数据包丢失的场景.常见 ...

  10. 使用VNC连接ubuntu16.4错误Authentication Failure问题

    解决办法:是因为vnc用一套自己的密码系统,不要去输入ssh登录时的密码,所以只需要进入远程服务器中,设置一哈vnc的密码即可! 在终端输入命令:vncpasswd 到此可以试试远程