首先,在 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. windbg配置问题汇总(记录)

    .loadby sos.dll mscorwks.symfix c:\windows\symbols windbg配置问题汇总 1.Failed to find runtime DLL (clr.dl ...

  2. k8s 操作命令(合集List)

    k8s 操作命令 合集List 一.K8S最常用命令如下: 1.获取pod信息:kubectl get pod 2.查看指定pod的日志信息:kubectl logs -f --tail(最后多少行) ...

  3. Parquet.Net: 将 Apache Parquet 移植到 .NET

    Parquet.Net 是一个用于读取和写入 Apache Parquet 文件的纯 .NET 库,使用MIT协议开源,github仓库:https://github.com/aloneguid/pa ...

  4. Servlet——执行流程、生命周期、方法介绍、体系结构

    执行流程    生命周期 对象的生命周期是指一个对象从被创建到被销毁的整个过程 Servlet运行在Servlet容器(web服务器)中,其生命周期由容器管理,分为四个阶段:     1. 加载和实例 ...

  5. namespace hdk

    没有高精类,因为这玩意太占内存了,正在优化 demap Rander StringAddition_InFix string ordered_vector #include<bits/stdc+ ...

  6. c++中的读写锁

    读写锁是一种特殊的锁机制,允许多个线程同时读取共享数据,但在写入共享数据时,只有一个线程可以进行写操作,其他线程必须等待. 这种机制对于读多写少的场景非常有效,可以提高并发性能.以下是通过 share ...

  7. /proc/pids/maps

    本实例中的用户空间地址从 0x00000000 到 0x80000000,从地址空间划分可知,从低到高依次是: 可执行文件的代码段.数据段.BSS段. 堆heap. 文件映射和匿名映射,包括vdso. ...

  8. 何为ORM框架?和ADO.NET 对比

    百度:ORM(Object Relational Mapping)框架采用元数据来描述对象与关系映射的细节,元数据一般采用XML格式,并且存放在专门的对象一映射文件中.简单理解为一种框架的格式 . 只 ...

  9. 在mac上配置nginx 并将前端的打包文件运行

    .markdown-body { line-height: 1.75; font-weight: 400; font-size: 16px; overflow-x: hidden; color: rg ...

  10. 为数据集而生的 SQL 控制台

    随着数据集的使用量急剧增加,Hugging Face 社区已经变成了众多数据集默认存放的仓库.每月,海量数据集被上传到社区,这些数据集亟需有效的查询.过滤和发现. 每个月在 Hugging Face ...