今日鸡汤:生活,一半诗意一半烟火,人生,一半努力一半随缘。努力做一个清醒,自律,坦荡的人。既要能够大步走天涯,也要可以浊酒伴清茶。
大家好,我是心灵。
最近看过我的文章的小伙伴都知道,在更新一个在Typescript中旅行的专栏,已经更新了5篇文章,这是第六篇,如果对你有帮助那就帮我点个赞。
- 学会satisfies操作符,让你的Typescript功力倍增!
- 一文盘点Typescript中23个内置类型工具! (建议收藏)
- 一行代码引发的离奇问题,众多大佬纷纷参与点评,最终Typescript之父出手解决
- 小红的Typescript之旅【入职】
- 小红学Typescript: 带你轻松掌握枚举🧐
今日更文《看一遍就理解Ts中的class》。
简介
ES2015中新增了class特性,类可以更方便的在使用js的时候进行面向对象编程。javascript中的类建立在原型之上,类实际上就是"特殊"的函数。Typescript对类添加了类型注释,并还增加一些独有的语法。
声明属性并初始化
在类中声明一个属性,使用构造函数进行初始化。
ts
class Person {
public readonly name: number
public age: number
constructor(name: number, age: number) {
this.name = name
this.age = age
}
}
Typescript对声明属性并初始化的操作提供了特殊的语法,将构造函数参数转换为具有相同名称类属性
ts
// 等价于上面的代码
class Person {
constructor(public readonly name: number, public age: number) {
// 自动做了this的赋值操作
}
}
构造函数重载
构造函数只用于初始化类。
- 构造函数重载可以对类对象提供不同的初始化方法。
- 在Typescript中不能给构造函数标注返回值类型,构造函数返回的始终都是类实例。
- 构造函数的参数可以有类型注解
ts
class Person {
name: string
age: number
// 构造函数重载
constructor(name: string): String // error 类型批注不能出现在构造函数中
constructor(name: string, age: number)
constructor(name: any, age?: any) {
this.name = name
this.age = age
}
}
声明成员属性
ts
class Person {
name;
age;
}
没有声明类型,那么name与age隐式都是any类型。
ts
class Person {
name: string; // 如果开启了strictPropertyInitialization报错:属性"name"没有初始化表达式,且未在构造函数中明确赋值
age: number; // 如果开启了strictPropertyInitialization报错:属性"age"没有初始化表达式,且未在构造函数中明确赋值
}
在类中声明变量并标注了类型,如果开启了strictPropertyInitialization: true
配置,那么就必须要初始化声明的变量。
ts
class Person {
name!: string;
age!: number;
}
使用断言语法明确的表明有值,不要进行赋值类型检测。
ts
class Person {
name = 'jack'
age = 18
}
p1.name // 鼠标悬浮显示 string
p1.age // 鼠标悬浮显示 number
如果设定了初始值,Typescript会自动推断其类型。
成员方法的重载
class中具有成员方法,成员方法的重载规则与函数重载一致。
ts
class Person {
add(x: number, y: number): number
add(x: string, y: string): string
add(x: any, y: any): any {
return x + y
}
}
const person = new Person()
person.add(1,2)
person.add('a','b')
implements
class类可以通过implements
关键字来实现接口。
ts
interface Runner {
id: number
run(): void
}
class Person implements Runner {
name!: string
id!: number
constructor(name: string) {
this.name = name
}
run() {
console.log(this.name + ' running...')
}
}
如果定义类时implements(实现)了接口,那么接口中定义的属性和方法都要在类中进行实现。
ts
interface Runner {
run(): void
}
interface Eat {
eat(): void
}
class Person implements Runner, Eat {
run() {}
eat() {}
}
类有可以实现多个接口。
ts
interface Runner {
run(): void
}
class Eat {
eat(): void {}
}
class Person implements Runner, Eat {
run() {}
eat() {}
}
implements关键字后也可以跟class声明的类,此时Person就要实现Eat类中的所有方法。如果Eat类作为接口被实现,那么Eat类中的成员只能用public关键字修饰(public是默认修饰符)
类的继承
extends 关键字可以在类声明的时候继承另一个类,创建出来的这个类是另一个类的子类。
ts
class Car {
brand!: string
constructor(brand: string) {
this.brand = brand
}
honk() {
console.log(this.brand + ' tuut!!')
}
display() {
console.log('car display')
}
}
class BMWCar extends Car {
constructor() {
super('bmw')
}
logBMWInfo() {
console.log('bwm is expand')
}
display(color?: string) {
if (color === undefined) {
super.display()
} else {
console.log('This bwm car is ' + color)
}
}
}
const bmw = new BMWCar()
bmw.honk() // 调用父类的方法
bmw.logBMWInfo() // 调用子类的方法
bmw.display() // 调用父类的display方法
bmw.display('red') // 调用该类本身的display方法
子类拥有父类中定义的属性和方法,而且可以定义其他的成员。
super关键字
- super在子类中指的是父类,如果在子类的构造函数中使用this关键字,那么必须调用 "super"关键字。可以先super关键字来调用父类的构造方法。
重写父类方法
- 我们在BMWCar这个子类中重写了display方法, 在子类的display的参数中有一个?关键字,这个?关键字是不可省的, 在Typescript中强制要求,重写的方法必须要满足父类中被重写的方法。
父类与子类的执行顺序
- 父类字段初始化
- 父类构造函数初始化
- 子类字段初始化
- 子类构造函数初始化
类中成员访问修饰符
public
- 类成员的默认可见性是public(如果什么都不写,那就是public),public修饰符代表可以在任何地方访问成员。
protected

- protected`修饰的成员仅类自身以及其子类可见
private

- private`只允许在子类中访问该成员。
私有化属性
ts
class Person {
private name: string = 'jack'
protected age: number = 18
}
const p1 = new Person()
const p1Name = p1['name']
console.log(p1Name) // 打印 "jack"
- 访问修饰符只能在ts文件中使用,所以只会在静态分析期间进行类型检查, 而且即使是被private修饰的成员,仍然可以通过方括号运算符来进行访问。可以使用JavaScript 的# 语法让属性在编译后也是私有状态,硬私有之后,方括号运算法也无法访问。
readonly成员修饰符
- readonly修饰符可以修饰字段,表达这个字段只读的

readonlu修饰的成员不可以在外部进行更改(除了构造函数内可以修改,其他地方都不可以更改)。
属性存取器Getter/Setter
- 类中字段存取器的作用主要是用于读写属性,并在获取值/设置值的过程中添加额外的逻辑。
ts
class Person {
private _age!: number
constructor(age: number) {
this.age = age
}
// get set 获取/访问值的时候的时候做一些拦截操作,比如set age的时候,判断年龄,如果<0的话,那么就直接抛出异常
get age() {
// doSomething...
return this._age
}
set age(age) {
if (age < 0) {
throw new Error('age cannot be less than 0')
}
this._age = age
}
}
const person = new Person(12)
console.log(person.age)
person.age = -10
console.log(person.age)
上面代码给age属性设置了存取器,当类实例访问age的时候,会触发get age()
方法,当类实例给age方法设置值的时候,会触发set age(age)
方法。
Typescript给存取器设置的规则:
- 如果
get
存在但不存在set
,则该属性自动为readonly
只读。 - 如果没有指定setter参数的类型,则从getter的返回类型推断。
- Getter 和 Setter 必须具有相同的访问可见性。
静态成员
- 类的静态成员与类的实例没有关系,通过类本身对静态成员进行访问
- 类的静态成员可以用
public
、protected
、private
修饰符来修饰 - 类的静态成员可以继承
ts
class Person {
static age = 12
private static sex: number
static getAge() {
console.log('person name')
}
}
Person.age
Person.getAge()
class Student extends Person {
}
Student.getAge() // ok
abstract 抽象类
- 使用abstract修饰一个类,那么这个类就是一个抽象类, 抽象类无法被实例化。
- 抽象类中可以定义抽象方法,抽象类相当于上层代码,有定义规范的作用。
- 抽象类可以包含抽象方法(只有方法的签名,没有具体实现),也可以包含普通方法,而其子类必须去实现抽象方法。
- 定义抽象方法的类,必须是一个抽象类。
ts
// 定义一个抽象类
abstract class Shape {
// 定义一个抽象方法,子类必须实现
abstract calculateArea(): number
// 普通方法
displayArea(): void {
const area = this.calculateArea()
console.log(`Area: ${area}`)
}
}
// 定义一个继承自抽象类的子类
class Circle extends Shape {
private radius!: number
constructor(radius: number) {
super()
this.radius = radius
}
// 实现抽象方法
calculateArea(): number {
return Math.PI * this.radius * this.radius
}
}
// 子类
class Rect extends Shape {
private width!: number
private height!: number
constructor(width: number, height: number) {
super()
this.width = width
this.height = height
}
// 实现抽象方法
calculateArea(): number {
return this.width * this.height
}
}
// 参数的类型是Shape基类
function getShapeArea(shape: Shape) {
return shape.displayArea()
}
getShapeArea(new Circle(5)) // 可以传入
getShapeArea(new Rect(2, 8)) // 可以传入
类在Typescript结构化类型系统中的表现
ts
class Person1 {
name = 'jack'
age = 18
}
class Person2 {
name = 'smith'
age = 20
}
这两个类只是长的像而已,并没有任何显式的关联。
const p1: Person1 = new Person2() // ok
声明变量的类型是Person1,将Person2的实例化对象赋值给Person1。上面的代码可以正常运行。
ts
class Person1 {
name:string
age: number
}
class Person2 {
name: string
age: number
sno: string // 学号
}
在Person2类中新增了一个属性。
const p1: Person1 = new Person2() // ok
上面的代码仍然可以正常运行。
ts
const p1: Person2 = new Person1() // 报错
将Person1的实例对象赋值给Person2,报错,类型 "Person1" 中缺少属性 "sno",但类型 "Person2" 中需要该属性。
虽然Person1与Person2并没有任何显式的关联,但是在结构化类型系统中,Person2是Person1的子类。
ts
class PirvatePerson1 {
private x: number
}
class PirvatePerson2 {
private x: number
}
let pp1 = new PirvatePerson1()
let pp2 = new PirvatePerson2()
pp1 = pp2 // error!
pp2 = pp1 // error!
当成员被private或protected修饰的时候,必须是来源于同一个类的实例才能相互赋值。也就是说结构化在这种情况下失效。
ts
class Empty { }
function foo(x: Empty) {}
foo(123) // ok
foo('baz') // ok
foo({}) // ok
定义了一个空类作为foo函数的参数类型,此时foo可以传递任何类型的参数。因为在结构化类型系统中,没有成员的类型是其他类型的超类型。
ts
interface Thing { }
function doSomething(a: Thing) {
// dosomething
}
doSomething(window); // ok
doSomething(42); // ok
doSomething('huh?'); // ok
定义一个空类型的interface与定义一个空类的表现基本一致,没有成员的类型可以接收任何类型。
所以,一般来说,不要定义空类与空的interface接口。
最后
本篇文章讲述了关于Typescript中的类,希望对你有帮助哈。为了阅读体验,篇幅没有很大,掌握以上内容应该能面对绝大部分场景了。在阅读过程中,如果有哪里不对的或者希望补充的,可以提出来哈,共同进步。
参考文献
[1]. TypeScript官网