如何TypeScript中相对优雅地实现类的多继承
首先,在 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";
// ...
如何TypeScript中相对优雅地实现类的多继承的更多相关文章
- Typescript中的可索引接口 类类型接口
/* 5.typeScript中的接口 可索引接口 类类型接口 */ /* 接口的作用:在面向对象的编程中,接口是一种规范的定义,它定义了行为和动作的规范,在程序设计里面,接口起到一种限制和规范的作用 ...
- 【TypeScript】如何在TypeScript中使用async/await,让你的代码更像C#。
[TypeScript]如何在TypeScript中使用async/await,让你的代码更像C#. async/await 提到这个东西,大家应该都很熟悉.最出名的可能就是C#中的,但也有其它语言也 ...
- TypeScript中的怪语法
TypeScript中的怪语法 如何处理undefined 和 null undefined的含义是:一个变量没有初始化. null的含义是:一个变量的值是空. undefined 和 null 的最 ...
- JavaScript 和 TypeScript 中的 class
对于一个前端开发者来说,很少用到 class ,因为在 JavaScript 中更多的是 函数式 编程,抬手就是一个 function,几乎不见 class 或 new 的踪影.所以 设计模式 也是大 ...
- typescript中的接口
说到接口:在面向对象的编程中,接口是一种规范的定义,它定义了行为和动作的规范,在程序设计里面,接口起到一种限制和规范的作用.接口定义了某一批类所需要遵守的规范,接口不关心这些类的内部状态数据,也不关心 ...
- Typescript中的装饰器原理
Typescript中的装饰器原理 1.小原理 因为react中的高阶组件本质上是个高阶函数的调用, 所以高阶组件的使用,我们既可以使用函数式方法调用,也可以使用装饰器. 也就是说,装饰器的本质就是一 ...
- React中如何优雅的捕捉事件错误
React中如何优雅的捕捉事件错误 前话 人无完人,所以代码总会出错,出错并不可怕,关键是怎么处理. 我就想问问大家react的错误怎么捕捉呢? 这个时候: 小白:怎么处理? 小白+: ErrorBo ...
- Kotlin(2): 优雅地扩展类的方法和属性
欢迎Follow我的GitHub, 关注我的CSDN. 个人博客: http://www.wangchenlong.org/, 最新内容. Kotlin由JetBrains公司推出, 是兼容Java的 ...
- Python项目中如何优雅的import
Python项目中如何优雅的import 前言 之前有一篇关于Python编码规范的随笔, 但是写的比较杂乱, 因为提到了import语句, 在篇文章中, 我专门来讲Python项目中如何更好的imp ...
- 十分钟教你理解TypeScript中的泛型
转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者.原文出处:https://blog.bitsrc.io/understanding-generics-in-t ...
随机推荐
- windbg配置问题汇总(记录)
.loadby sos.dll mscorwks.symfix c:\windows\symbols windbg配置问题汇总 1.Failed to find runtime DLL (clr.dl ...
- k8s 操作命令(合集List)
k8s 操作命令 合集List 一.K8S最常用命令如下: 1.获取pod信息:kubectl get pod 2.查看指定pod的日志信息:kubectl logs -f --tail(最后多少行) ...
- Parquet.Net: 将 Apache Parquet 移植到 .NET
Parquet.Net 是一个用于读取和写入 Apache Parquet 文件的纯 .NET 库,使用MIT协议开源,github仓库:https://github.com/aloneguid/pa ...
- Servlet——执行流程、生命周期、方法介绍、体系结构
执行流程 生命周期 对象的生命周期是指一个对象从被创建到被销毁的整个过程 Servlet运行在Servlet容器(web服务器)中,其生命周期由容器管理,分为四个阶段: 1. 加载和实例 ...
- namespace hdk
没有高精类,因为这玩意太占内存了,正在优化 demap Rander StringAddition_InFix string ordered_vector #include<bits/stdc+ ...
- c++中的读写锁
读写锁是一种特殊的锁机制,允许多个线程同时读取共享数据,但在写入共享数据时,只有一个线程可以进行写操作,其他线程必须等待. 这种机制对于读多写少的场景非常有效,可以提高并发性能.以下是通过 share ...
- /proc/pids/maps
本实例中的用户空间地址从 0x00000000 到 0x80000000,从地址空间划分可知,从低到高依次是: 可执行文件的代码段.数据段.BSS段. 堆heap. 文件映射和匿名映射,包括vdso. ...
- 何为ORM框架?和ADO.NET 对比
百度:ORM(Object Relational Mapping)框架采用元数据来描述对象与关系映射的细节,元数据一般采用XML格式,并且存放在专门的对象一映射文件中.简单理解为一种框架的格式 . 只 ...
- 在mac上配置nginx 并将前端的打包文件运行
.markdown-body { line-height: 1.75; font-weight: 400; font-size: 16px; overflow-x: hidden; color: rg ...
- 为数据集而生的 SQL 控制台
随着数据集的使用量急剧增加,Hugging Face 社区已经变成了众多数据集默认存放的仓库.每月,海量数据集被上传到社区,这些数据集亟需有效的查询.过滤和发现. 每个月在 Hugging Face ...