6、TypeScript
面向对象
6.1、认识类的使用
在早期的JavaScript开发中(ES5)我们需要通过函数和原型链来实现类和继承,从ES6开始,引入了class关键字,可以更加方 便的定义和使用类。
TypeScript作为JavaScript的超集,也是支持使用class关键字的,并且还可以对类的属性和方法等进行静态类型检测。
实际上在JavaScript的开发过程中,我们更加习惯于函数式编程:
- 比如React开发中,目前更多使用的函数组件以及结合Hook的开发模式;
- 比如在Vue3开发中,目前也更加推崇使用 Composition API;
但是在封装某些业务的时候,类具有更强大封装性,所以我们也需要掌握它们。
类的定义我们通常会使用class关键字:
- 在面向对象的世界里,任何事物都可以使用类的结构来描述;
- 类中包含特有的属性和方法;
6.2、类的基本使用
我们来定义一个Person类:
- 使用class关键字来定义一个类;
我们可以声明类的属性:在类的内部声明类的属性以及对应的类型
- 如果类型没有声明,那么它们默认是any的;
- 我们也可以给属性设置初始化值;
- 在默认的strictPropertyInitialization模式下面我们的属性是必须 初始化的,如果没有初始化,那么编译时就会报错;
- 如果我们在strictPropertyInitialization模式下确实不希望给属 性初始化,可以使用 name!: string语法;
类可以有自己的构造函数constructor,当我们通过new关键字创建 一个实例时,构造函数会被调用;
- 构造函数==不需要返回任何值,默认返回当前创建出来的实例;==
类中可以有自己的函数,定义的函数称之为方法;
class Person {
// 成员属性: 声明成员属性
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
eating() {
console.log(this.name + " eating")
}
running() {
console.log(this.name + " running")
}
}
// 实例对象: instance
const p1 = new Person("why", 18)
const p2 = new Person("kobe", 30)
console.log(p1.name, p2.age)
export {}
6.3、类的继承
面向对象的其中一大特性就是继承,继承不仅仅可以减少我们的代码量,也是多态的使用前提
我们使用extends关键字来实现继承,子类中使用super来访问父类。
我们来看一下Student类继承自Person:
- Student类可以有自己的属性和方法,并且会继承Person的属性和方法;
- 在构造函数中,我们可以通过super来调用父类的构造方法,对父类中的属性进行初始化;
6.4、类的成员修饰符
在TypeScript中,类的属性和方法支持三种修饰符: public、private、protected
- public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的;
- private 修饰的是仅在同一类中可见、私有的属性或方法;
- protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法;
public是默认的修饰符,也是可以直接访问的,我们这里来演示一下protected和private。
// public
// private
// protected
class Person {
protected name: string
private age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
// 方法变成私有方法: 只有在类内部才能访问
private eating() {
console.log("吃东西", this.age, this.name)
}
}
const p = new Person("why", 18)
// console.log(p.name, p.age)
// p.name = "kobe"
// p.eating()
// 子类中是否可以访问
class Student extends Person {
constructor(name: string, age: number) {
super(name, age)
}
studying() {
console.log("在学习", this.name)
}
}
const stu = new Student("why", 18)
6.5、只读属性readonly
如果有一个属性我们不希望外界可以任意的修改,只希望确定值后直接使用,那么可以使用readonly:
class Person {
readonly name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
// 类和实例之间的关系(重要)
const p = new Person("why", 18)
console.log(p.name, p.age)
// p.name = "kobe" 只读属性不能进行写入操作
p.age = 20
export {}
6.6、getters/setters
class Person {
// 私有属性: 属性前面会使用_
private _name: string
private _age: number
constructor(name: string, age: number) {
this._name = name
this._age = age
}
running() {
console.log("running:", this._name)
}
// setter/getter: 对属性的访问进行拦截操作
set name(newValue: string) {
this._name = newValue
}
get name() {
return this._name
}
set age(newValue: number) {
if (newValue >= 0 && newValue < 200) {
this._age = newValue
}
}
get age() {
return this._age
}
}
const p = new Person("why", 100)
p.name = "kobe"
console.log(p.name)
p.age = -10
console.log(p.age)
export {}
6.7、参数属性(Parameter Properties)
TypeScript 提供了特殊的语法,可以把一个构造函数参数转成一个同名同值的类属性。
- 这些就被称为参数属性(parameter properties);
- 你可以通过在构造函数参数前添加一个可见性修饰符 public private protected 或者 readonly 来创建参数属性,最后这些类 属性字段也会得到这些修饰符;
class Person{
//语法糖
constructor(public name:string,private _age:number){
}
set age(newAge){
this._age=newAge
}
get age(){
return this._age
}
}
6.8、抽象类abstract
abstract表示是抽象的,他是一个关键字,以前的时候讲过关键字 const/let/switch/class/funciton
保留字表示为目前来说可能还没有用到、未来的话可能会提升为一个关键字,保留字或者关键字都不能作为是变量的名字
我们知道,继承是多态使用的前提
- 所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。
- 但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法。
什么是 抽象方法? 在TypeScript中没有具体实现的方法(没有方法体),就是抽象方法
- 抽象方法,必须存在于抽象类中;
- 抽象类是使用abstract声明的类;
抽象类有如下的特点:
- 抽象类是不能被实例的话(也就是不能通过new创建)
- 抽象类可以包含抽象方法,也可以包含有实现体的方法
- 抽象方法必须被子类实现,否则该类必须是一个抽象类;
- 抽象方法必须被子类实现,否则该类必须是一个抽象类;
abstract class Shape {
// getArea方法只有声明没有实现体
// 实现让子类自己实现
// 可以将getArea方法定义为抽象方法: 在方法的前面加abstract
// 抽象方法必须出现在抽象类中, 类前面也需要加abstract
abstract getArea()
}
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
}
}
class Triangle extends Shape {
getArea() {
return 100
}
}
// 通用的函数
function calcArea(shape: Shape) {
return shape.getArea()
}
calcArea(new Rectangle(10, 20))
calcArea(new Circle(5))
calcArea(new Triangle())
// 在Java中会报错: 不允许
calcArea({ getArea: function() {} })
// 抽象类不能被实例化
// calcArea(new Shape())
// calcArea(100)
// calcArea("abc")
6.9、鸭子类型
TypeScript对于类型检测的时候、他使用的是鸭子类型、他并不是一种严格的类型检测、他只关心里边的属性或者行为是否一样
// TypeScript对于类型检测的时候使用的鸭子类型
// 鸭子类型: 如果一只鸟, 走起来像鸭子, 游起来像鸭子, 看起来像鸭子, 那么你可以认为它就是一只鸭子
// 鸭子类型, 只关心属性和行为, 不关心你具体是不是对应的类型
class Person {
constructor(public name: string, public age: number) {}
running() {}
}
class Dog {
constructor(public name: string, public age: number) {}
running() {}
}
function printPerson(p: Person) {
console.log(p.name, p.age)
}
printPerson(new Person("why", 18))
// printPerson("abc")
printPerson({name: "kobe", age: 30, running: function() {}})
printPerson(new Dog("旺财", 3))
const person: Person = new Dog("果汁", 5)
export {}
6.1、类的类型
类本身也是可以作为一种数据类型的:
/**
* 类的作用:
* 1.可以创建类对应的实例对象
* 2.类本身可以作为这个实例的类型
* 3.类也可以当中有一个构造签名的函数
*/
class Person{
name:string
constructor(name:string){
this.name=name;
}
running(){
console.log(this.name+"running")
}
}
//name的类型是String
const name:string="aa"
//类本身可以作为这个实例的类型
const p:Person=new Person()
function printPerson(p:Person){}
//类也可以当作有一个构造签名的函数
function factory(ctor:new ()=>void){}//构造签名的函数表达式形式
factory(Person)//Person是一个类/结论(类的话可以当成是一个有构造签名的函数)
6.2、对象类型的属性修饰符
对象类型中的每个属性可以说明它的类型、属性是否可选、属性是否只读等信息。
可选属性(Optional Properties)
- 我们可以在属性名后面加一个?标记表示这个属性是可以选的
只读属性(Readonly Properties)
- 在 TypeScript 中,属性可以被标记为 readonly,这不会改变任何运行时的行为;
- 但在类型检查的时候,一个标记为 readonly的属性是不能被写入的。
// 定义对象类型
type IPerson = {
// 属性?: 可选的属性
name?: string
// readonly: 只读的属性
readonly age: number
}
interface IKun {
name?: string
readonly slogan: string
}
const p: IPerson = {
name: "why",
age: 18
}
// p.age = 30
export {}
6.3、索引签名(Index Signatures)
什么是索引签名?
在TypeScript中,索引签名(Index Signature)是用来描述对象可以具有动态属性的一种方式。它可以让你定义一个接口或类型,允许对象拥有任何类型的字符串或数字索引,同时约束这些动态属性的值类型。
索引签名分为两种 1、字符串索引签名 2、数字索引签名
1、字符串索引签名
字符串索引签名允许对象通过字符串键来访问其属性,它通常用于模拟映射或字典的行为。声明字符串索引签名的语法如下:
//语法格式
interface StringIndexedObject {
[index: string]: ValueType;
}
//示例
interface Person {
name: string;
age: number;
[otherInfo: string]: string | number; // 允许额外的任意字符串键,其值可以是string或number类型
}
// 创建一个实现了 Person 接口的实例
let person: Person = {
name: 'Alice',
age: 30,
address: '123 Main St', // 这是一个额外的字符串键,其值为字符串类型,符合索引签名的要求
height: 165, // 这也是一个额外的字符串键,其值为数字类型,同样符合索引签名的要求
};
console.log(person.name); // 输出: Alice
console.log(person.age); // 输出: 30
console.log(person.address); // 输出: 123 Main St
console.log(person.height); // 输出: 165
在这个示例中,我们创建了一个 Person
类型的实例 person
,它除了具有 name
和 age
属性外,还有额外的 address
和 height
属性,这两个属性都是通过索引签名 [otherInfo: string]: string | number;
来支持的。
2、数字索引签名
数字索引签名则用于表示对象可以通过数字键访问其属性,这通常用于数组-like的对象。声明数字索引签名的语法如下
interface NumberIndexedObject {
[index: number]: ValueType;
}
// 示例:
interface Matrix {
length: number;
[index: number]: number; // 允许通过数字索引访问的值必须是number类型
}
注意事项:
1、一个对象只能有一个字符串索引签名和一个数字索引签名(两者之一或同时存在)。
2、当存在索引签名时,对象也可以有明确命名的属性。
3、索引签名本身不强制对象一定要有那些动态属性,它只是规定了如果有这些属性,则其类型必须符合索引签名的约束。
案例
// 定义一个包含数字索引签名的 Matrix 接口
interface Matrix {
length: number;
[index: number]: number; // 允许通过数字索引访问的值必须是number类型
}
// 创建一个实现了 Matrix 接口的实例
const matrix: Matrix = {
length: 3, // 表示矩阵有3个元素
0: 1, // 第一个元素
1: 2, // 第二个元素
2: 3, // 第三个元素
};
// 访问和修改矩阵元素
matrix[0] = 4; // 修改第一个元素
console.log(matrix[0]); // 输出: 4
// 打印整个矩阵的元素
for(let i = 0; i < matrix.length; i++) {
console.log(matrix[i]);
}
在这个示例中,matrix
是一个实现了 Matrix
接口的对象,它的属性可以通过数字索引访问。尽管看上去像是一个简单的对象,但由于存在数字索引签名,我们可以像操作数组那样去访问和修改它的元素。注意,尽管行为类似数组,但在JavaScript中它并不是真正的数组,因此不能直接使用数组的方法,如 .map()
、.forEach()
等,除非自行扩展或转换成数组。
3、混合使用
interface MixedIndexSignature {
id: number;
name: string;
[dynamicProp: string]: any; // 字符串索引签名
[arrayIndex: number]: string[]; // 数字索引签名
}
在这个示例中,MixedIndexSignature
接口除了有两个明确命名的属性 id
和 name
外,还允许存在任意数量的字符串键(其值可以是任何类型)和数字键(其值必须是 string[]
类型)。
示例
// 定义一个包含混合索引签名的接口
interface MixedIndexSignature {
id: number;
name: string;
[dynamicProp: string]: any; // 它允许对象拥有任意数量的字符串键属性,并且这些属性的值可以是任何类型(即 any 类型)。
[arrayIndex: number]: string[]; // 数字索引签名,要求值为字符串数组
}
// 创建一个实现了 MixedIndexSignature 接口的实例
let mixedObj: MixedIndexSignature = {
id: 1,
name: 'Example Object',
customData: { key: 'value' }, // 利用字符串索引签名添加动态属性
0: ['Item 1'], // 利用数字索引签名添加一个字符串数组
1: ['Item 2', 'Item 3'] // 同样利用数字索引签名添加另一个字符串数组
};
// 访问和修改对象属性
mixedObj['additionalInfo'] = { more: 'data' }; // 利用字符串索引签名添加更多动态属性
mixedObj[1].push('Item 4'); // 修改数字索引签名对应的字符串数组
console.log(mixedObj.name); // 输出: Example Object
console.log(mixedObj.customData); // 输出: { key: 'value' }
console.log(mixedObj[0]); // 输出: ['Item 1']
console.log(mixedObj[1]); // 输出: ['Item 2', 'Item 3', 'Item 4']
注意
1、一个索引签名的属性必须是string或者是number
2、虽然TypeScript可以同时支持string和number类型,但是数字索引的返回值类型一定要是字符索引返回类型的子类型(了解)
解读
在TypeScript中,一个接口或类型可以同时定义字符串索引签名和数字索引签名,但有一定的规则约束。具体来说,当一个类型同时定义了数字索引签名和字符串索引签名时,数字索引签名的返回值类型(数组元素类型)必须是字符串索引签名返回值类型的子类型。这是因为JavaScript中数字索引可以隐式转化为字符串索引,所以在类型系统中也要求数字索引的值类型不能比字符串索引的值类型更宽泛。
这个规则保证了无论使用字符串索引还是数字索引,都能确保访问到的值类型具有一致性或兼容性。
举例说明1:
// 正确的示例:数字索引签名的返回值类型(number)是字符串索引签名返回值类型(string | number)的子类型
interface MyType {
[key: string]: string | number; // 字符串索引签名,允许任意字符串键,值可以是string或number类型
[index: number]: number; // 数字索引签名,值必须是number类型
}
let myObj: MyType = {
name: 'Alice', // 字符串键值
0: 123, // 数字键值
'age': 25 // 字符串键值
};
myObj[0]; // 返回值类型为 number,符合数字索引签名的规定
myObj['name']; // 返回值类型为 string | number,也满足字符串索引签名的规定,因为number是string | number的子类型
错误示例1:
// 错误的示例:数字索引签名的返回值类型(boolean)不是字符串索引签名返回值类型(string)的子类型
interface BadType {
[key: string]: string; // 字符串索引签名,值必须是string类型
[index: number]: boolean; // 数字索引签名,值必须是boolean类型,这不是string类型的子类型
}
// TypeScript会在此处抛出错误,因为数字索引签名与字符串索引签名不满足子类型关系
let badObj: BadType = {
name: 'Alice', // 字符串键值
0: true, // 数字键值,但类型不符合规则
};
错误示例2:
interface NotOkay{
[x1:number]:Animal;
[x2:string]:Dog //因该换一下 遵循结论:number类型索引返回类型要是字符串索引类型返回类型的子类型
}
'numebr' index type 'Animal' is not assignable to 'string'index type 'Dog'
错误示例3:
interface BadType {
[index: number]: string;
[key: string]: number|string;
aa:boolean; //这个会报错
}
const names:IIndexType=["abc","cba","nba"]
const item1=names[0]
const forEac=names["forEach"]
// aa:boolean 并不符合 返回boolean并不是number|string
names["aaaa"]
4、看一个细节点(严格字面量赋值检测)
interface IIndexType{
//返回值类型的目的是告知通过索引获取到的值是什么类型的
[index:string]:string
}
const names:IIndexType=["abc","cba","nba"]
names.forEach() //namesp[forEach]==>function 返回值是一个函数
exprot {}
多余的属性不符合返回值是string类型的值类型、你会发现当你的返回值如果是any就不报错了。
6.4、接口的继承
封装Axios的时候可能会用到这个东西
interface定义的对象类型其实是一种接口、可以实现继承的特性、Type是没有继承的特性的
interface IPerson{
name:string
age:number
}
// 可以从其他的接口中继承过来的属性
// 1、减少了相同代码的重复编写
// 2、如果我们使用的是一个第三方库,给我们定义了一些属性,自定义一个接口,同时你希望自定义的接口拥有第三方某个类型中所有的属性
// 可以使用继承来完成
interface IKun extends IPerson{
slogan:string
}
const ikun:IKun={
name:"why",
age:18
slogan:"你干嘛,哎呦喂"
}
export {}
6.5、接口的实现
interface IKun{
name:string
age:number
slogan:string
playBasketball:()=>void
}
//1、作用:直接作为类型
cont ikun:IKun={
name:"why",
age:18,
slogan:"你干嘛!"
playBasketball:funcation(){}
}
interface IRun{
running:()=>void
}
//2、作用:接口被类实现
// 一个类可以实现多个接口
class Person implements IKun,IRun{
name:string
age:number
slogan:string
playBasketball(){}
}
const ikun2=new Person()
console.log(ikun.name,ikun2.age,ikun2.slogan)
ikun2.playBasketball()
6.6、抽象类和接口的区别
抽象类在很大程度上和接口会有点类似:都可以在其中定义一个方法,让子类或实现类来实现对应的方法。
那么抽象类和接口有什么区别呢?
- 抽象类是事物的抽象,抽象类用来捕捉子类的通用特性,接口通常是一些行为的描述;
- 抽象类通常用于一系列关系紧密的类之间,接口只是用来描述一个类应该具有什么行为;
- 接口可以被多层实现,而抽象类只能单一继承;
- 抽象类中可以有实现体,接口中只能有函数的声明;
通常我们会这样来描述类和抽象类、接口之间的关系:
- 抽象类是对事物的抽象,表达的是 is a 的关系。猫是一种动物(动物就可以定义成一个抽象类)
- 接口是对行为的抽象,表达的是 has a 的关系。猫拥有跑(可以定义一个单独的接口)、爬树(可以定义一个单独的接口) 的行为。
6.7、严格字面量赋值检测
interface IPerson{
name:string
age:number
}
//奇怪的现象一
const info:IPerson={
name:"why"
age:18
// 多了一个height属性
height: 1.88 //会提示报错
}
// 单独把对象拿出来
const info2={
name:"why"
age:18
// 多了一个height属性
height: 1.88
}
// 这个对象字面量扩散了,就不新鲜了
const duixiang:IPerson=info2
//奇怪的现象二
funcation printPerson(preson:IPerson){
}
printPerson({name:"kobe",age:30,height:1.98})//报错
const kobe={name:"kobe",age:30,height:1.98}
printPerson(kobe)//不符合却不报错
为什么会这样?
6.8、TypeScript枚举类型
枚举类型是为数不多的TypeScript特性有的特性之一:
- 枚举其实就是将一组可能出现的值,一个个列举出来,定义在一个类型中,这个类型就是枚举类型;
- 枚举允许开发者定义一组命名常量,常量可以是数字、字符串类型;
enum Direction{
LEFT,
RIGHT,
TOP,
BOOTOM
}
function turnDirection(direction:Direction){
switch(direction){
case Direction.LEFT:
console.log("转向左边~")
case Direction.RIGHT:
console.log("转向右边~")
case Direction.TOP:
console.log("转向上边~")
case Direction.BOTTOM:
console.log("转向下边~")
default:
const myDirection:never=direction;
}
}
6.9、枚举类型的值
枚举类型默认是有值的,比如上面的枚举,默认值是这样的
当然,我们也可以给枚举其他值:
- 这个时候会从100进行递增;
我们也可以给他们赋值其他的类型:
//默认不赋值是有默认值
enum Direction{
LEFT=0
RIGHT=1,
TOP=2,
BOTTOM=3
}
//后边的数值是依次递增得
enum Direction{
LEFT=100,
RIGHT,
TOR,
BOTTOM
}
//也可以为枚举类型赋值字符串
enum Direction{
LEFT,
RIGHT,
TOP="TOP"
BOTTOM="BOTTOM"
}