探秘Typescript·Class类型的补充
前言
我们已经了解了Typescript的一些常用类型的相关知识,在梳理这些知识时,我们或多或少也有涉及到一些Class类型的知识,不过基本都是惊鸿一瞥,未曾深入,因此,在这里单独聊一下在Typescript中的Class的相关知识。
类的定义与类的成员
首先,我们先来通过一段示例代码了解一下Typescript中类的定义和成员的基础用法
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
showInfo(msg: string): void {
console.log(`hi,${msg}, my name is ${this.name}, I am ${this.age} years old!`)
}
}
const p: Person = new Person('kiner', 20);
p.showInfo("man");
从上面可以看到,我们可以为一个类型的成员属性定义类型描述,如属性name和age,也可以为这个类的方法设定参数和返回值等,这些都是在Typescript当中类的最基础用法。
成员函数(方法)的重载
在之前探秘Typescript·函数类型的补充这篇文章当中,我们知道了函数时可以被重载的,那么,同理,在类里面的成员函数(即方法)也是可以被重载的。
type Info = {msg: string, cnt: number};
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
showInfo(info: Info): void;
showInfo(info: string, cnt: number): void;
showInfo(info: string | Info, cnt?: number): void {
if(typeof info === "string") {
console.log(`hi,${info}, my name is ${this.name}, I am ${this.age} years old!, cur cnt is ${cnt}`)
} else {
console.log(`hi,${info.msg}, my name is ${this.name}, I am ${this.age} years old!, cur cnt is ${info.cnt}`)
}
}
}
const p: Person = new Person('kiner', 20);
p.showInfo("man", 3);
p.showInfo({msg: "boy", cnt: 5});
如上述示例,我们的showInfo方法有 2 次类型重载和一个具体实现。在具体的开发时,我们的IDE也会根据重载给出多种代码提示:
倘若我们没有按照上面重载的方式进行传参,IDE也会直接报错,避免我们进行一些非法使用。提升代码的健壮性。
需要注意的是,当我们闷在实现函数重载时,函数的具体实现时的参数数量需要去适配所有重载中参数数量最多的那一个,比如上面的例子,我们的重载中参数数量最多的是2个,因此我们再后续实现时,需要适配两个参数的场景,而1个参数的场景其实也包含在里面了。
总结一下:使用成员函数(方法)的重载我们可以获得更好的书写提示体验和更严谨的代码校验提示
索引器
如果你想让自己定义的一个类像数组一样可以形如arr[10] = 100这样调用,我们可以使用索引器
class Arr<T> {
[i: number]: T
}
const arr = new Arr<number>();
arr[10] = 100;
arr[11] = 'ddd';// 报错,只能复制数字类型数据
利用索引器,我们可以在一个自定义的类中实现类似数组索引的赋值和取值的操作,提升开发与使用体验。
继承
类型的继承
我们在学习Typescript的基础类型时,有学习过interface的继承,这就是类型的继承,我们来简单复习一下:
interface Person {
name: string;
age: number;
eat: (food: string): void
}
interface Student extends Person {
stdNo: number;
learn(subject: string): void;
}
// Student 接口通过继承 Person 获得了 Person 的属性和方法,还可以额外实现自己的方法和定义自己的属性
类的继承
class Person {
private name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public eat(food: string): void {
console.log(`${this.name} is eatting ${food}`)
}
}
class Student extends Person {
private stdNo: number;
constructor(name: string, age: number, stdNo: number) {
super(name, age);
this.stdNo = stdNo;
}
public learn(subject: string): void {
console.log(`${name} is learning ${subject}`)
}
}
const stu = new Student('kiner', 28, 2021060703316);
stu.eat("rice");
stu.learn('chinese')
上面就是最基础的类的继承示例,但在实际使用的过程中,我们不太推荐使用这种方法,因为这样耦合性还是很强,我们更推荐使用组合的方式设计类
interface Eatable {
eat: (food: string) => void;
}
interface Learnable {
learn: (subject: string) => void;
}
class Person implements Eatable {
eat: (food: string) => void;
}
class Student implements Eatable, Learnable {
eat: (food: string) => void;
learn: (subject: string) => void;
}
这样将不同的行为抽象到不同的接口中,需要哪些接口的方法我们就去实现一下,这样能够更好的达到解耦的目的。
除此之外,我们还可以考虑使用泛型去设计
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Learnable<T extends Person> {
target: T;
constructor(target: T) {
this.target = target;
}
learn(subject: string) {
console.log(`${this.target.name} is learning ${subject}`);
}
}
通过泛型,我们将Learnable的目标对象局限于Person及其子类,既可以解耦,又可以灵活变化。
总结一下:继承会加重代码耦合,而组合与泛型不会,因此在实际系统设计时,更推荐采用组合或泛型的方式
成员可见域
-
public:该成员允许所有程序访问,如果显示声明可见域,默认可见域就是public -
protected:可在当前类以及其子类中使用,实例化对象不能使用该成员。 -
private:只能在当前类内部使用,子类以及实例化对象无法使用该成员// 通常如果将一个类型设置为 private,如果外部想要获取或设置,则需要自定义实现 get 或 set 方法 class Person { private name: string; public getName(): string { return this.name; } public setName(name: string): void { this.name = name; } }
静态成员
在一些类当中会有一些静态成员,静态成员跟类的实例没有关系,无论是静态成员还是静态方法都是直接绑定在类上的。
class Person {
static cnt = 0;
static getCnt(): number {
return Person.cnt;
}
}
Person.cnt = 1;
console.log(Person.getCnt());// 1
Person.cnt = 2;
console.log(Person.getCnt());// 2
静态成员也是可以被继承的。
class Person {
static cnt = 0;
static getCnt(): number {
return Person.cnt;
}
}
class Student extends Person {
_cnt = Person.getCnt()
}
需要注意的是,并不是所有的属性都可以被设置为变态成员,在Typescript当中,name属性时被保留的静态成员,你不能去覆盖他:
class Person {
static name = 'kiner';// Error
}
// 由于 class 在 JavaScript 当中,本质上是 Function,而 Function 本身就有一个静态成员是 name,即当前方法的名字,因此我们不能去覆盖他
类型守卫
class FileSystem {
isFile(): this is FileRep {
return this instanceof FileRep
}
}
const fs = new FileSystem();
if(fs.isFile()) {
// 在此处,我们 fs 的类型已经被窄化为 FileRep 了,可以直接使用 FileRep 的方法和属性
}
抽象类
抽象类就是不可以被实例化,只能被继承的类,如:
abstract class Person {
abstract getName(): string;
showName(): void {
console.log(`hi, ${this.getName()}`);
}
}
// const p1 = new Person();// 报错,因为抽象类是不能被实例化的
// 正确用法
class Student extends Person {
private name: string
constructor(name: string) {
super();
this.name = name;
}
getName(): string {
return this.name;
}
}
const stu = new Student("kiner");
stu.showName();
类型的关系
在Typescript是通过判断类成员是否完全一致类判断两个类是否一样的,因此,类似下面这个实例,实际上在Typescript中是不会报错的,这个需要特别注意一下:
class Point1 {
x = 0;
y = 0;
}
class Point2 {
x = 0;
y = 0;
}
const p: Point1 = new Point2();
console.log(p.x);
// 虽然 p 被指定是 Point1 类型,但却接收到了 Point2 的实例化对象,但由于这两个类的成员是完全一样的,Typescript认为他们其实是一样的,就不会报错。
即使成员不是完全一样,只要Point1的成员是Point2的子集,也是允许的,如:
class Point1 {
x = 0;
y = 0;
}
class Point2 {
x = 0;
y = 0;
z = 0;
}
const p: Point1 = new Point2();
console.log(p.x);
但如果反过来Point2是Point1的子集就不行了。
结语
至此,相信大家已经对于 Typescript 当中的 Class 的类型有了一定的了解了,相信大家应该也能够更加游刃有余的处理自己负责项目当中的一些类型了,需要特别注意的是,为了让我们的代码能够更加地严谨以及增加累的封装新,建议在写 Class 类型是,都尽量增加上 private、public、readonly 等限制修饰符,这样能够更大程度地避免我们的程序在运行期间遇到一些奇奇怪怪的问题,在一些大型工程项目当中,其他人使用时,也能够更加规范地按照要求去使用,而不是想 js 一样,怎么使用都行。