如何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 ...
随机推荐
- Fluent Builder 模式
前言 以前最讨厌设计复杂方法调用, 就是那种需要一堆有逻辑规则的 config 作为参数的方法. 这种 config 通常是一个大对象, 有许多 property, property 之间有存在一些逻 ...
- C#/.NET/.NET Core开发实战教程集
DotNetGuide介绍 DotNetGuide是一个专注于C#/.NET/.NET Core学习.工作.面试指南的GitHub知识库,该知识库在GitHub中Star数已突破6.5k+当然这离不开 ...
- mysql-存储过程(1) mysql循环语句
mysql循环语句: 本文总结了mysql常见的三种循环方式:while.repeat和loop循环.还有一种goto,不推荐使用. 一.while循环 delimiter // #定义标识符为双斜杠 ...
- thinkphp在原字段上面进行加减操作
经常有需要对某个数据表的计数字段进行加减操作,我们来看下在ThinkPHP中的具体使用办法. 最简单的,使用下面方法对score自加,第二个参数也可以不要,默认加1: M('User')-> ...
- balance_dirty_pages_ratelimited分析
balance_dirty_pages_ratelimited分析 nr_dirtied_pause:当前task的脏页门限: dirty_exceeded:全局的脏页数超过门限或者该bdi的脏页数超 ...
- Redis数据库常见命令
Redis数据库常见命令 Linux启动Redis # 启动服务 redis-server # 开启客户端 redis-cli # 关闭redis服务 shutdown #查看服务是否运行 ping ...
- 2023年3月中国数据库排行榜:开源OTO揽获前三,传统达梦、GBase触机便发
东风何时至,已绿湖上山. 春风送来了2023年3月的 墨天轮中国数据库流行度排行,本月共有260个数据库参与排名,本月榜单前十可以用一句话概括为:榜单前八较上月岿然不动,GBase 奋勇向前重返第九. ...
- C++ 第一节课 名字空间 ,输入输出函数,和 C 语言的区别
#include <iostream> // #include 头文件,C++标准库的头文件都不带 .h (.h 是C库头文件添加的) #include <cstdio> #i ...
- wpf之样式
在Window.Resources中书写样式 : <Window.Resources> <Style TargetType="Button" > </ ...
- go frame资源管理打包失败
最近有个需求,需要使用golang做一个小工具,然后我就想既然是小工具,那就把前后端放在一个二进制文件中.恰好使用的项目架构是go frame,它已经提供了这样的能力,但是没想到碰到了一鼻子灰... ...