8. TS Class
类(class)是面向对象编程的基本构件,封装了属性和方法,TypeScript 给予了全面支持。
类的属性可以在顶层声明,也可以在构造方法内部声明。
TypeScript 有一个配置项strictPropertyInitialization,只要打开(默认是打开的),就会检查属性是否设置了初值,如果没有就报错。
// 打开 strictPropertyInitialization
class Point {
x: number; // 报错
y: number; // 报错
}
上面示例中,如果类的顶层属性不赋值,就会报错。如果不希望出现报错,可以使用非空断言
class Point {
x!: number;
y!: number;
}
上面示例中,属性x和y没有初值,但是属性名后面添加了感叹号,表示这两个属性肯定不会为空,所以 TypeScript 就不报错了.
属性名前面加上 readonly 修饰符,就表示该属性是只读的。构造方法可以修改这个属性,实例对象不能修改这个属性。
构造方法不能声明返回值类型,否则报错,因为它总是返回实例对象。
存取器(getter/setter)
存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法。它们用于读写某个属性,取值器用来读取属性,存值器用来写入属性。
如果某个属性只有get方法,没有set方法,那么该属性自动成为只读属性。
get方法与set方法的可访问性必须一致,要么都为公开方法,要么都为私有方法。
interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。
interface Country {
name:string;
capital:string;
}
// 或者
type Country = {
name:string;
capital:string;
}
class MyCountry implements Country {
name = '';
capital = '';
}
类MyCountry使用implements关键字,表示该类的实例对象满足这个外部类型。interface 只是指定检查条件,如果不满足这些条件就会报错。它并不能代替 class 自身的类型声明。(implements 只是一种“类型约束”——它 检查类是否满足接口结构,但 并不会为类生成类型本身,也不能决定类的完整类型。)
让一个类去实现implements某个接口,其实是让你的类满足interface定义的标准。
类可以定义接口没有声明的方法和属性。
implements关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。
注意,interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为 TypeScript 设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法。
类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。
class Car implements MotorVehicle, Flyable, Swimmable {
// ...
}
但是,同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以使用两种方法替代:类的继承或接口继承 extends
TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。
TypeScript 的类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型。
class Color {
name:string;
constructor(name:string) {
this.name = name;
}
}
const green:Color = new Color('green');
上面示例中,定义了一个类Color。它的类名就代表一种类型,实例对象green就属于该类型。
对于引用实例对象的变量来说,既可以声明类型为 Class,也可以声明类型为 Interface,因为两者都代表实例对象的类型。
interface MotorVehicle {
}
class Car implements MotorVehicle {
}
// 写法一
const c1:Car = new Car();
// 写法二
const c2:MotorVehicle = new Car();
它们的区别是,如果类Car有接口MotorVehicle没有的属性和方法,那么只有变量c1可以调用这些属性和方法。
作为类型使用时,类名只能表示实例的类型,不能表示类的自身类型。
易混淆点:
class Foo {
id!: number;
}
// 接收实例 ✅
function useInstance(arg: Foo) {
console.log(arg.id);
}
// 接收类(构造函数) ✅
function useClass(ClassRef: typeof Foo) {
const obj = new ClassRef();
console.log(obj.id);
}
Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class
一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。
class Foo {
id!:number;
}
function fn(arg:Foo) {
// ...
}
const bar = {
id: 10,
amount: 100,
};
fn(bar); // 正确
如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合。
class Person {
name: string;
}
class Customer {
name: string;
}
// 正确
const cust:Customer = new Person();
Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。
不仅是类,如果某个对象跟某个 class 的实例结构相同,TypeScript 也认为两者的类型相同。
确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法
如果类中存在私有成员(private)或保护成员(protected),那么确定兼容关系时,TypeScript 要求私有成员和保护成员来自同一个类,这意味着两个类需要存在继承关系。
类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法。
根据结构类型原则,子类也可以用于类型为基类的场合。
子类可以覆盖基类的同名方法。
class B extends A {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name}`);
}
}
}
使用super关键字指代基类是常见做法。这里表示调用A的greet方法。
子类的同名方法不能与基类的类型定义相冲突。
public修饰符表示这是公开成员,外部可以自由访问。
private修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。严格地说,private定义的私有成员,并不是真正意义的私有成员。一方面,编译成 JavaScript 后,private关键字就被剥离了,这时外部访问该成员就不会报错。另一方面,由于前一个原因,TypeScript 对于访问private成员没有严格禁止,使用方括号写法([])或者in运算符,实例对象就能访问该成员。 ES2022 引入了自己的私有成员写法#propName。因此建议不使用private,改用 ES2022 的写法,获得真正意义的私有成员。
protected修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。
实例属性的简写形式:
实际开发中,很多实例属性的值,是通过构造方法传入的。
class Point {
x:number;
y:number;
constructor(x:number, y:number) {
this.x = x;
this.y = y;
}
}
上面实例中,属性x和y的值是通过构造方法的参数传入的。
这样的写法等于对同一个属性要声明两次类型,一次在类的头部,另一次在构造方法的参数里面。这有些累赘,TypeScript 就提供了一种简写形式。
class Point {
constructor(
public x:number,
public y:number
) {}
}
const p = new Point(10, 10);
p.x // 10
p.y // 10
上面示例中,构造方法的参数x前面有public修饰符,这时 TypeScript 就会自动声明一个公开属性x,不必在构造方法里面写任何代码,同时还会设置x的值为构造方法的参数值。注意,这里的public不能省略。
除了public修饰符,构造方法的参数名只要有private、protected、readonly修饰符,都会自动声明对应修饰符的实例属性。
class A {
constructor(
public a: number,
protected b: number,
private c: number,
readonly d: number
) {}
}
// 编译结果
class A {
a;
b;
c;
d;
constructor(a, b, c, d) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}
}
类的内部可以使用static关键字,定义静态成员。
静态成员是只能通过类本身使用的成员,不能通过实例对象使用。
TypeScript 允许在类的定义前面,加上关键字abstract,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abstract class)。
抽象类只能当作基类使用,用来在它的基础上定义子类。
抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类。
抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有abstract关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。
抽象成员只能存在于抽象类,不能存在于普通类。
抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加abstract关键字。
抽象成员前也不能有private修饰符,否则无法在子类中实现该成员。
一个子类最多只能继承一个抽象类。
总之,抽象类的作用是,确保各种相关的子类都拥有跟基类相同的接口,可以看作是模板。其中的抽象成员都是必须由子类实现的成员,非抽象成员则表示基类已经实现的、由所有子类共享的成员。
类的方法经常用到this关键字,它表示该方法当前所在的对象。
评论