likes
comments
collection
share

「Typescript之旅」:看一遍就理解Ts中的class

作者站长头像
站长
· 阅读数 3

今日鸡汤:生活,一半诗意一半烟火,人生,一半努力一半随缘。努力做一个清醒,自律,坦荡的人。既要能够大步走天涯,也要可以浊酒伴清茶。

大家好,我是心灵。

今日更文《看一遍就理解Ts中的class》。

简介

ES2015中新增了class特性,类可以更方便的在使用js的时候进行面向对象编程。javascript中的类建立在原型之上,类实际上就是“特殊”的函数。Typescript对类添加了类型注释,并还增加一些独有的语法。

声明属性并初始化

在类中声明一个属性,使用构造函数进行初始化。

class Person {
	public readonly name: number
	public age: number
	constructor(name: number, age: number) {
		this.name = name
		this.age = age
	}
}

Typescript对声明属性并初始化的操作提供了特殊的语法,将构造函数参数转换为具有相同名称类属性

// 等价于上面的代码
class Person {
	constructor(public readonly name: number, public age: number) {
		// 自动做了this的赋值操作
	}
}

构造函数重载

构造函数只用于初始化类。

  1. 构造函数重载可以对类对象提供不同的初始化方法。
  2. 在Typescript中不能给构造函数标注返回值类型,构造函数返回的始终都是类实例。
  3. 构造函数的参数可以有类型注解
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
	}
}

声明成员属性

class Person {
	name;
	age;
}

没有声明类型,那么name与age隐式都是any类型。

class Person {
  name: string; // 如果开启了strictPropertyInitialization报错:属性“name”没有初始化表达式,且未在构造函数中明确赋值
  age: number;  // 如果开启了strictPropertyInitialization报错:属性“age”没有初始化表达式,且未在构造函数中明确赋值
}

在类中声明变量并标注了类型,如果开启了strictPropertyInitialization: true配置,那么就必须要初始化声明的变量。

class Person {
  name!: string;
  age!: number;
}

使用断言语法明确的表明有值,不要进行赋值类型检测。

class Person {
	name = 'jack'
	age = 18
}

p1.name // 鼠标悬浮显示 string
p1.age // 鼠标悬浮显示 number

如果设定了初始值,Typescript会自动推断其类型。

成员方法的重载

class中具有成员方法,成员方法的重载规则与函数重载一致。

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关键字来实现接口。

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(实现)了接口,那么接口中定义的属性和方法都要在类中进行实现。

interface Runner {
	run(): void
}

interface Eat {
	eat(): void
}
class Person implements Runner, Eat {
	run() {}
	eat() {}
}

类有可以实现多个接口。

interface Runner {
	run(): void
}

class Eat {
	eat(): void {}
}
class Person implements Runner, Eat {
	run() {}
	eat() {}
}

implements关键字后也可以跟class声明的类,此时Person就要实现Eat类中的所有方法。如果Eat类作为接口被实现,那么Eat类中的成员只能用public关键字修饰(public是默认修饰符)

类的继承

extends 关键字可以在类声明的时候继承另一个类,创建出来的这个类是另一个类的子类。

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中强制要求,重写的方法必须要满足父类中被重写的方法。

父类与子类的执行顺序

  1. 父类字段初始化
  2. 父类构造函数初始化
  3. 子类字段初始化
  4. 子类构造函数初始化

类中成员访问修饰符

public

  • 类成员的默认可见性是public(如果什么都不写,那就是public),public修饰符代表可以在任何地方访问成员。

protected

「Typescript之旅」:看一遍就理解Ts中的class

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

private

「Typescript之旅」:看一遍就理解Ts中的class

  • private`只允许在子类中访问该成员。

私有化属性

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修饰符可以修饰字段,表达这个字段只读的

「Typescript之旅」:看一遍就理解Ts中的class

readonlu修饰的成员不可以在外部进行更改(除了构造函数内可以修改,其他地方都不可以更改)。

属性存取器Getter/Setter

  • 类中字段存取器的作用主要是用于读写属性,并在获取值/设置值的过程中添加额外的逻辑。
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给存取器设置的规则:

  1. 如果get存在但不存在set,则该属性自动为readonly只读。
  2. 如果没有指定setter参数的类型,则从getter的返回类型推断。
  3. Getter 和 Setter 必须具有相同的访问可见性。

静态成员

  1. 类的静态成员与类的实例没有关系,通过类本身对静态成员进行访问
  2. 类的静态成员可以用publicprotectedprivate修饰符来修饰
  3. 类的静态成员可以继承
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 抽象类

  1. 使用abstract修饰一个类,那么这个类就是一个抽象类, 抽象类无法被实例化。
  2. 抽象类中可以定义抽象方法,抽象类相当于上层代码,有定义规范的作用。
  3. 抽象类可以包含抽象方法(只有方法的签名,没有具体实现),也可以包含普通方法,而其子类必须去实现抽象方法。
  4. 定义抽象方法的类,必须是一个抽象类。
// 定义一个抽象类
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结构化类型系统中的表现

class Person1 {
	name = 'jack'
	age = 18
}

class Person2 {
	name = 'smith'
	age = 20
}

这两个类只是长的像而已,并没有任何显式的关联。

const p1: Person1 = new Person2() // ok

声明变量的类型是Person1,将Person2的实例化对象赋值给Person1。上面的代码可以正常运行。

class Person1 {
	name:string
	age: number
}

class Person2 {
	name: string
	age: number
	sno: string // 学号
}

在Person2类中新增了一个属性。

const p1: Person1 = new Person2() // ok

上面的代码仍然可以正常运行。

const p1: Person2 = new Person1() // 报错

将Person1的实例对象赋值给Person2,报错,类型 "Person1" 中缺少属性 "sno",但类型 "Person2" 中需要该属性。

虽然Person1与Person2并没有任何显式的关联,但是在结构化类型系统中,Person2是Person1的子类。

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修饰的时候,必须是来源于同一个类的实例才能相互赋值。也就是说结构化在这种情况下失效。

class Empty { }

function foo(x: Empty) {}

foo(123) // ok
foo('baz') // ok
foo({}) // ok

定义了一个空类作为foo函数的参数类型,此时foo可以传递任何类型的参数。因为在结构化类型系统中,没有成员的类型是其他类型的超类型。

interface Thing { }
function doSomething(a: Thing) {
  // dosomething
}
doSomething(window); // ok
doSomething(42); // ok
doSomething('huh?'); // ok

定义一个空类型的interface与定义一个空类的表现基本一致,没有成员的类型可以接收任何类型。

所以,一般来说,不要定义空类与空的interface接口。

最后

本篇文章讲述了关于Typescript中的类,希望对你有帮助哈。为了阅读体验,篇幅没有很大,掌握以上内容应该能面对绝大部分场景了。在阅读过程中,如果有哪里不对的或者希望补充的,可以提出来哈,共同进步。

参考文献

[1]. TypeScript官网