Skip to content

TS 面向对象

认识类的使用

在早期的 JavaScript 开发中(ES5)我们需要通过函数和原型链来实现类和继承,从 ES6 开始,引入了class关键字,可以更加方 便的定义和使用类

TypeScript 作为 JavaScript 的超集,也是支持使用 class 关键字的,并且还可以对类的属性和方法等进行静态类型检测

实际上在 JavaScript 的开发过程中,我们更加习惯于函数式编程:

  • 比如 React 开发中,目前更多使用的函数组件以及结合 Hook 的开发模式;
  • 比如在 Vue3 开发中,目前也更加推崇使用 Composition API;

类的定义我们通常会使用 class 关键字:

  • 在面向对象的世界里,任何事物都可以使用类的结构来描述;
  • 类中包含特有的属性和方法

类的定义

ts
class Person {
	// 成员属性: 声明成员属性
	name: string = ''
	age: number = 0

	constructor(name: string, age: number) {
		this.name = name
		this.age = age
	}
}

// 实例对象: instance
const p1 = new Person('sj', 19)
console.log(p1) // Person { name: 'sj', age: 19 }

类的继承

我们使用 extends 关键字来实现继承,子类中使用 super 来访问父类。

我们来看一下 Student 类继承自 Person:

  • Student 类可以有自己的属性和方法,并且会继承 Person 的属性和方法;
  • 在构造函数中,我们可以通过 super 来调用父类的构造方法,对父类中的属性进行初始化;
ts
class Person {
	name: string
	age: number
	constructor(name: string, age: number) {
		this.name = name
		this.age = age
	}
}

class Student extends Person {
	sno: number

	constructor(name: string, age: number, sno: number) {
		super(name, age)
		this.sno = sno
	}

	studying() {
		console.log(this.sno + 'studying')
	}
}
const p = new Person('sj', 19)
console.log(p) // Person { name: 'sj', age: 19 }
const p1 = new Student('sj', 19, 99)
console.log(p1) // Student { name: 'sj', age: 19, sno: 99 }
p1.studying()

类的成员修饰符

在 TypeScript 中,类的属性和方法支持三种修饰符: public、private、protected

  • public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是 public 的;
  • private 修饰的是仅在同一类中可见、私有的属性或方法;
  • protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法;

public 是默认的修饰符,也是可以直接访问的,我们这里来演示一下 protected 和 private。

ts
class Person {
	// 修饰的是仅在类自身及子类中可见、受保护的属性或方法;
	protected name: string
	// 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的
	public age: number

	constructor(name: string, age: number) {
		this.name = name
		this.age = age
	}
	// e 修饰的是仅在同一类中可见、私有的属性或方法;
	private eating() {
		console.log('吃饭了', this.name)
	}
}

const p = new Person('sj', 19)
console.log(p) // Person { name: 'sj', age: 19 }

class Student extends Person {
	constructor(name: string, age: number) {
		super(name, age)
	}
}
const p1 = new Student('XQ', 19)
console.log(p1) // Student { name: 'XQ', age: 19 }

只读属性 readonly

ts
class Person {
	readonly name: string
	age: number

	constructor(name: string, age: number) {
		this.name = name
		this.age = age
	}
}

// 类和实例的关系
const p = new Person('sj', 19)
console.log(p.name, p.age)
// 只读属性不能写入
p.name = 'XQ' // 报错

getters/setters

ts
class Person {
	// 私有属性(内部): 私有属性属性前面加 _
	private _name: string

	constructor(name: string) {
		this._name = name
	}

	running() {
		console.log('running', this._name)
	}

	// getters/setters: 对属性的访问进行拦截操作
	set name(newValue: string) {
		this._name = newValue
	}
	get name() {
		return this._name
	}
}

// 类和实例的关系
const p = new Person('sj')
console.log(p.name) // sj
p.running() // running sj
// setters
p.name = 'XQ'
// getters
console.log(p.name) //XQ

export {}

参数属性

TypeScript 提供了特殊的语法,可以把一个构造函数参数转成一个同名同值的类属性。

  • 这些就被称为参数属性(parameter properties);
  • 你可以通过在构造函数参数前添加一个可见性修饰符 public private protected 或者 readonly 来创建参数属性,最后这些类 属性字段也会得到这些修饰符;
ts
class Person {
	// 语法糖
	constructor(public name: string, private _age: number) {
		this.name = name
		this._age = _age
	}

	running() {
		console.log('running', this.name) // running sj
	}

	set age(newValue: number) {
		this._age = newValue
	}
	get age() {
		return this._age
	}
}

const p = new Person('sj', 19)
console.log(p) // Person { name: 'sj', age: 19 }
p.age = 20 // 20
console.log(p.age)
p.running()

export {}

抽象类 abstract

我们知道,继承是多态使用的前提。

  • 所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。
  • 但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法。

什么是 抽象方法? 在 TypeScript 中没有具体实现的方法(没有方法体),就是抽象方法。

  • 抽象方法,必须存在于抽象类中;
  • 抽象类是使用 abstract 声明的类;

抽象类有如下的特点:

  • 抽象类是不能被实例的话(也就是不能通过 new 创建)
  • 抽象类可以包含抽象方法,也可以包含有实现体的方法;
  • 有抽象方法的类,必须是一个抽象类;
  • 抽象方法必须被子类实现,否则该类必须是一个抽象类;
ts
abstract class shape {
	// getArea 方法只有声明没有实现体
	// 实现体让子类自己实现
	// 可以将方法定义为抽象方法: 在方法前面添加 abstract
	// 抽象方法必须出现在抽象对象中,所以类也加 abstract
	abstract getArea(): number
}

class Rectangle extends shape {
	constructor(public width: number, public height: number) {
		super()
	}
	getArea() {
		return this.width * this.height
	}
}

class Circle extends shape {
	constructor(public radius: number) {
		super()
	}
	getArea() {
		return this.radius ** 2 * Math.PI
	}
}

// 通用函数
function calcArea(shape: shape) {
	// 调用子类的方法
	const res = shape.getArea()
	console.log(res)
	return res
}

calcArea(new Rectangle(10, 10)) // 314.1592653589793
calcArea(new Circle(10)) // 314.1592653589793

// calcArea(new shape()) 抽象类不能有实例

类的类型

ts
class Person {}
/**
 * 类的作用:
 * 1.可以创建类对应的实例对象
 * 2.类本身可以作为这个实例的类型
 * 3.类可以当做有一个构造签名的函数
 */

const name: string = 'aaa'
const p: Person = new Person()
function PrintPerson(p: Person) {}

function factory(ctor: new () => void) {}
factory(Person)

对象类型的修饰符

对象类型中的每个属性可以说明它的类型、属性是否可选、属性是否只读等信息。

可选属性(Optional Properties):

  • 我们可以在属性名后面加一个 ? 标记表示这个属性是可选的;

只读属性(Readonly Properties)

  • 在 TypeScript 中,属性可以被标记为 readonly,这不会改变任何运行时的行为;
  • 但在类型检查的时候,一个标记为 readonly 的属性是不能被写入的
ts
// 定义对象类型
type IPerson = {
	// 属性 ? :可选类型
	name?: string
	// readonly: 只读属性
	readonly age: number
}

interface ILike {
	like: string
}

const p: IPerson = {
	name: 'sj',
	age: 19,
}
p.age = 20 // 报错(只读)
console.log(p)

索引签名

ts
// 1.索引签名的理解
interface IInfoType {
	// 所以签名: 通过字符串获取一个值,也是字符串
	[key: string]: string
	// [index: string]: number
}

function getInfo(): IInfoType {
	const name: any = 'sj'
	return name
}

const info = getInfo()
console.log(info) // sj

// 2.索引签名的案例
interface ICollection {
	[index: number]: string
	length: number
}

function printCollection(collection: ICollection) {
	for (let i = 0; i < collection.length; i++) {
		const item = collection[i]
		console.log(item)
		console.log(item.length)
	}
}

const names = ['aaa', 'bbb', 'ccc'] // 索引 0 1 2 => number
const tuple: [string, string] = ['sj', '19'] // 索引 0 1 => number
printCollection(names)
printCollection(tuple)

export {}

接口继承

ts
interface IPerson {
	name: string
	age: number
}
/**
 * 继承属性:
 * 1.减少了相同代码的重复编写
 * 2.如果使用第三方库,给我们自定义了一些属性
 * > 自定义一个接口,同时你希望拥有第三方中的类型
 * > 可以通过继承来完成你需要的第三方属性
 */
interface ILike extends IPerson {
	like: string[]
}

const sj: ILike = {
	name: 'sj',
	age: 19,
	like: ['抽烟', '喝酒', '打麻将'],
}

console.log(sj) // { name: 'sj', age: 19, like: [ '刷抖音', '抽烟', '打麻将' ] }

接口的实现

ts
interface ISj {
	name: string
	age: number
	like: () => void
}

const sj: ISj = {
	name: 'sj',
	age: 19,
	like: function () {
		console.log('Hello World')
	},
}
console.log(sj) // { name: 'sj', age: 19, like: [Function: like] }

// 作用: 接口被类实现
class Person implements ISj {
	name: string = 'XQ'
	age: number = 19
	like: () => void = function () {
		console.log('Hello World')
	}
}

const ISj2 = new Person()
console.log(ISj2.name, ISj2.age) // XQ 19
ISj2.like() // Hello World

严格字面量赋值检查

错误:

ts
interface IPerson {
	name: string
	age: number
}

const p: IPerson = {
	name: 'sj',
	age: 19,
	like: ['抽烟', '喝酒', '打麻将'], // 报错
}

function printInfo(info: IPerson) {
	console.log(info.name, info.age, info.like) // 报错
}

printInfo({ name: 'sj', age: 19, like: ['抽烟', '喝酒', '打麻将'] }) // 报错

正确:

ts
interface IPerson {
	name: string
	age: number
}

const p = {
	name: 'sj',
	age: 19,
	like: ['抽烟', '喝酒', '打麻将'],
}
// 解释现象:
// 第一次场景对象字面量,称为fresh(新鲜)
// 对象新鲜的字面量严格检查,后面不在进行严格检查
const info: IPerson = p
console.log(info) // { name: 'sj', age: 19, like: [ '抽烟', '喝酒', '打麻将' ] }

function printInfo(info: any) {
	console.log(info.name, info.age, info.like) // sj 19 [ '抽烟', '喝酒', '打麻将' ]
}

const sj = { name: 'sj', age: 19, like: ['抽烟', '喝酒', '打麻将'] }
printInfo(sj)

抽象类和接口的区别(了解)

抽象类在很大程度上和接口会有点类似:都可以在其中定义一个方法,让子类或实现类来实现对应的方法

那么抽象类和接口有什么区别呢?

  • 抽象类是事物的抽象,抽象类用来捕捉子类的通用特性,接口通常是一些行为的描述;
  • 抽象类通常用于一系列关系紧密的类之间,接口只是用来描述一个类应该具有什么行为;
  • 接口可以被多层实现,而抽象类只能单一继承;
  • 抽象类中可以有实现体,接口中只能有函数的声明;

通常我们会这样来描述类和抽象类、接口之间的关系:

  • 抽象类是对事物的抽象,表达的是 is a 的关系。猫是一种动物(动物就可以定义成一个抽象类)
  • 接口是对行为的抽象,表达的是 has a 的关系。猫拥有跑(可以定义一个单独的接口)、爬树(可以定义一个单独的接口) 的行为。

TypeScript 枚举类型

枚举其实就是将一组可能出现的值,一个个列举出来,定义在一个类型中,这个类型就是枚举类型;

枚举允许开发者定义一组命名常量,常量可以是数字、字符串类型.

ts
// 定义一个枚举类型
enum Direction {
	UP,
	DOWN,
	LEFT,
	RIGHT,
}

const d1: Direction = Direction.LEFT
console.log(d1) // 2

function turnDirection(direction: Direction) {
	switch (direction) {
		case Direction.UP:
			console.log('向上移动')
			break
		case Direction.DOWN:
			console.log('向下移动')
			break
		case Direction.LEFT:
			console.log('向左移动')
			break
		case Direction.RIGHT:
			console.log('向右移动')
			break
	}
}

// 监听键盘点击
turnDirection(Direction.LEFT) // 向左移动

export {}

枚举类型的值

枚举类型默认是有值的,比如上面的枚举,默认值是这样的:

当然,我们也可以给枚举其他值:

  • 这个时候会从 100 进行递增;
ts
// 定义一个枚举类型
// 默认
enum Direction {
	TOP = 0,
	BOTTOM = 1,
	LEFT = 2,
	RIGHT = 3,
}
// 赋值 后面会自动加 100
enum Direction {
	TOP = 100,
	BOTTOM,
	LEFT,
	RIGHT,
}
// 赋值
enum Direction {
	TOP = 'TOP',
	BOTTOM = 'BOTTOM',
	LEFT = 'LEFT',
	RIGHT = 'RIGHT',
}

const d1: Direction = Direction.TOP

export {}