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

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

大家好,我是心灵。

最近看过我的文章的小伙伴都知道,在更新一个在Typescript中旅行的专栏,已经更新了5篇文章,这是第篇,如果对你有帮助那就帮我点个赞。

今日更文《看一遍就理解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的赋值操作
	}
}

构造函数重载

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

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

父类与子类的执行顺序

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

类中成员访问修饰符

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给存取器设置的规则:

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

静态成员

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

  1. 使用abstract修饰一个类,那么这个类就是一个抽象类, 抽象类无法被实例化。
  2. 抽象类中可以定义抽象方法,抽象类相当于上层代码,有定义规范的作用。
  3. 抽象类可以包含抽象方法(只有方法的签名,没有具体实现),也可以包含普通方法,而其子类必须去实现抽象方法。
  4. 定义抽象方法的类,必须是一个抽象类。
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官网](https://link.juejin.cn?target=https%3A%2F%2Fwww.typescriptlang.org%2F "https://www.typescriptlang.org/")

相关推荐
Lupino26 分钟前
被 React “玩弄”的 24 小时:为了修一个不存在的 Bug,我给大模型送了顿火锅钱
前端·react.js
米丘32 分钟前
了解 Javascript 模块化,更好地掌握 Vite 、Webpack、Rollup 等打包工具
前端
Heo34 分钟前
深入 React19 Diff 算法
前端·javascript·面试
滕青山35 分钟前
个人所得税计算器 在线工具核心JS实现
前端·javascript·vue.js
小怪点点36 分钟前
手写promise
前端·promise
国思RDIF框架44 分钟前
RDIFramework.NET Web 敏捷开发框架 V6.3 发布 (.NET8+、Framework 双引擎)
前端
颜酱1 小时前
从0到1实现LFU缓存:思路拆解+代码落地
javascript·后端·算法
Mintopia1 小时前
如何在有限的时间里,活出几倍的人生
前端
炫饭第一名1 小时前
速通Canvas指北🦮——变形、渐变与阴影篇
前端·javascript·程序员
Neptune11 小时前
让我带你迅速吃透React组件通信:从入门到精通(上篇)
前端·javascript