三、TypeScript面向对象

2022/9/4 TypeScript类的成员修饰符抽象类索引签名接口继承严格字面量赋值检测枚举类型enum

# 1、认识类的使用

  • 在早期的JavaScript开发中(ES5)我们需要通过函数和原型链来实现类和继承,从ES6开始,引入了class关键字,可以更加方便的定义和使用类。
  • TypeScript作为JavaScript的超集,也是支持使用class关键字的,并且还可以对类的属性和方法等进行静态类型检测。
  • 实际上在JavaScript的开发过程中,我们更加习惯于函数式编程:
    • 比如React开发中,目前更多使用的函数组件以及结合Hook的开发模式;
    • 比如在Vue3开发中,目前也更加推崇使用 Composition API;
  • 但是在封装某些业务的时候,类具有更强大封装性,所以我们也需要掌握它们。
  • 类的定义我们通常会使用class关键字:
    • 在面向对象的世界里,任何事物都可以使用类的结构来描述;
    • 类中包含特有的属性和方法;

# 2、类的定义

  • 我们来定义一个Person类:
    • 使用class关键字来定义一个类;
  • 我们可以声明类的属性:在类的内部声明类的属性以及对应的类型
    • 如果类型没有声明,那么它们默认是any的;
    • 我们也可以给属性设置初始化值;
    • 在默认的strictPropertyInitialization(严格属性初始化)模式下面我们的属性是必须初始化的,如果没有初始化,那么编译时就会报错;【具体设置该模式,在tsconfig.json文件中设置】
      • 如果我们在strictPropertyInitialization模式下确实不希望给属性初始化,可以使用 name!: string语法;
  • 类可以有自己的构造函数constructor,当我们通过new关键字创建一个实例时,构造函数会被调用;
    • 构造函数不需要返回任何值,默认返回当前创建出来的实例;
  • 类中可以有自己的函数,定义的函数称之为方法;
class Person {
  name!: string // 非空类型断言,表示确定name属性有值
  age: number = 23 //我们也可以给属性设置初始化值;
  height//如果类型没有声明,那么它们默认是any的; -  ts提示:成员 "height" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型。(推荐这里写具体类型)
  constructor(name: string, age: number, height: number, address: string) {
    this.name = name 
    this.age = age
    this.height = height 
    this.address = address // ts报错提示: 类型“Person”上不存在属性“address”。【所以需要在Person类型中定义属性address】
  } 

  running() {// 实例方法
    console.log(this.name,'running')
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3、类的继承

  • 面向对象的其中一大特性就是继承,继承不仅仅可以减少我们的代码量,也是多态的使用前提。
  • 我们使用extends关键字来实现继承,子类中使用super来访问父类。
  • 我们来看一下Student类继承自Person:
    • Student类可以有自己的属性和方法,并且会继承Person的属性和方法;
    • 在构造函数中,我们可以通过super来调用父类的构造方法,对父类中的属性进行初始化;
class Person {
  name: string
  age: number
  constructor(name: string, age: number) {
    this.name = name 
    this.age = age
  }

  running() {
    console.log(this.name,"running Person")
  }

  static yk() {
    console.log('Person的类方法yk')
  }
}

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

  studying() {
    console.log(this.name,'studying Student')
  }


  running() {//父类方法的重写
    super.running()//调用父类的实例方法
    console.log(this.name,"running Student")
  }
}

const p1 = new Person('kobe',45)
const stu1 = new Student('james',38,2236042401)
p1.running()//kobe running Person
stu1.running()//james running Person  -  james running Student
Student.yk()//Person的类方法yk
console.log(Object.getPrototypeOf(Student) === Person)//true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 4、类的成员修饰符

  • 在TypeScript中,类的属性和方法支持三种修饰符: public、private、protected
    • public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的;
    • private 修饰的是仅在同一类中可见、私有的属性或方法;
    • protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法;
  • public是默认的修饰符,也是可以直接访问的,我们这里来演示一下protected和private。
class Person {
  public name: string //公有属性
  private age: number  //私有属性:只能在类的内部使用
  protected address: string  //受保护属性:只能在类和子类的内部使用
  constructor(name:string,age:number,address:string) {
    this.name = name
    this.age = age
    this.address = address
  }

  private running() {
    console.log('Person的私有实例方法:running')
  }
  protected studying() {
    console.log('Person的私有实例方法:studying')
  }
}

class Student extends Person {
  constructor(name:string,age:number,address:string) {
    super(name,age,address)
  }

  private eating() {
    super.running()// ts报错提示:属性“running”为私有属性,只能在类“Person”中访问。
    super.studying()
    console.log('Student的私有实例方法:eating')
  }
}
const p1 = new Person("james",38,'洛杉矶')
const stu1 = new Student('kobe',45,'洛杉矶')
console.log(stu1,p1)
console.log(p1.name)// james
console.log(p1.age)// ts报错提示:属性“age”为私有属性,只能在类“Person”中访问。
console.log(p1.address)// ts报错提示:属性“address”受保护,只能在类“Person”及其子类中访问。
console.log(stu1.name)// kobe
console.log(stu1.age)// ts报错提示:属性“age”为私有属性,只能在类“Person”中访问。
console.log(stu1.address)// ts报错提示:属性“address”受保护,只能在类“Person”及其子类中访问。
p1.running()// ts报错提示:属性“running”为私有属性,只能在类“Person”中访问。
p1.studying()// ts报错提示: 属性“studying”受保护,只能在类“Person”及其子类中访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 5、只读属性readonly

  • 如果有一个属性我们不希望外界可以任意的修改,只希望确定值后直接使用,那么可以使用readonly:
class Person {
  readonly name: string
  constructor(name: string) {
    this.name = name
  }
}
const p = new Person('kobe')
console.log(p.name)// kobe
p.name = 'james' // ts报错提示:无法分配到 "name" ,因为它是只读属性
1
2
3
4
5
6
7
8
9

# 6、getters/setters

  • 在前面一些私有属性我们是不能直接访问的,或者某些属性我们想要监听它的获取(getter)和设置(setter)的过程,这个时候我们可以使用存取器。
class Person {
  private _name: string
  constructor(name: string) {
    this._name = name
  }

  get name() {
    console.log('获取私有属性_name值')
    return this._name
  }
  
  set name(newValue) {
    console.log('设置私有属性_name值为:',newValue)
    this._name = newValue
  }
}

const p1 = new Person('kobe')
console.log(p1)//Person { _name: 'rose' }
// console.log(p1._name)// ts报错提示:属性“_name”为私有属性,只能在类“Person”中访问。
console.log(p1.name)//调用get name实例方法
p1.name = 'rose'//调用set name实例方法
console.log(p1.name)//调用get name实例方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 7、参数属性(Parameter Properties)

  • TypeScript 提供了特殊的语法,可以把一个构造函数参数转成一个同名同值的类属性。
    • 这些就被称为参数属性(parameter properties);
    • 你可以通过在构造函数参数前添加一个可见性修饰符 public private protected 或者 readonly 来创建参数属性,最后这些类属性字段也会得到这些修饰符;(语法糖写法)
//1.之前的写法
class Person {
  public name: string
  private age: number
  protected address: string
  readonly sno: number
  constructor(name: string, age: number, address: string, sno: number) {
    this.name = name 
    this.age = age
    this.address = address
    this.sno = sno
  }
}
console.log(new Person('james',38,'上海市',2200002))//Person { name: 'james', age: 38, address: '上海市', sno: 2200002 }

//2.参数属性写法(相当于之前的语法糖写法)
class Person1 {
  constructor(public name: string, private age: number, protected address: string, readonly sno: number) {}
}
console.log(new Person1('kobe',45,'北京市',2200001))//Person1 { name: 'kobe', age: 45, address: '北京市', sno: 2200001 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 8、抽象类abstract

  • 我们知道,继承是多态使用的前提。
    • 所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。
    • 但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法。
  • 什么是 抽象方法? 在TypeScript中没有具体实现的方法(没有方法体),就是抽象方法。
    • 抽象方法,必须存在于抽象类中;
    • 抽象类是使用abstract声明的类;
  • 抽象类有如下的特点:
    • 抽象类是不能被实例的话(也就是不能通过new创建)
    • 抽象方法必须被子类实现,否则该类必须是一个抽象类;

# 9、抽象类演练(该案例体现了多态)

abstract class Shape { //抽象类
  abstract getArea(): number//抽象方法
}

function calcArea(shape: Shape) {
  console.log(shape.getArea())
}

class Rectangle extends Shape {//矩形面积
  constructor(public width:number, public height:number) {
    super()
    this.width = width
    this.height = height
  }

  getArea(): number {
    return this.width * this.height
  }
}


class Circle extends Shape {//圆的面积
  constructor(public r:number) {
    super()
    this.r = r
  }

  getArea(): number {
    return this.r * this.r * Math.PI
  }
}

calcArea(new Rectangle(10,20))//200
calcArea(new Circle(10))//314.1592653589793
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 10、类的类型

  • 类本身也是可以作为一种数据类型的:
class Person {
  constructor(public name: string) {}
  running() {
    console.log(this.name,",running")
  }
}

const p1: Person = new Person("kobe")
const p2: Person = {
  name:'james',
  running() {
    console.log('running')
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 11、对象类型的属性修饰符(Property Modifiers)

  • 对象类型中的每个属性可以说明它的类型、属性是否可选、属性是否只读等信息。
  • 可选属性(Optional Properties)
    • 我们可以在属性名后面加一个 ? 标记表示这个属性是可选的;
  • 只读属性(Readonly Properties)
    • 在 TypeScript 中,属性可以被标记为 readonly,这不会改变任何运行时的行为;
    • 但在类型检查的时候,一个标记为 readonly的属性是不能被写入的。
interface IPerson {
  name: string
  age?: number
  readonly address: string
}

const p: IPerson = {
  name:'kobe',
  address:'广州'
}
p.address = '上海' // ts报错提示: 无法分配到 "address" ,因为它是只读属性。
1
2
3
4
5
6
7
8
9
10
11

# 12、索引签名(Index Signatures)

  • 索引签名的基本使用(索引里面有很多的细节点;具体没有详细说明的,可以看ts官方文档)
type ArrType = {
  [index: number]:string //索引签名:表示key为number类型,value为string类型;即数组
}

interface ArrType1 {
  [index: string]:string //索引签名:表示key为string类型,value为string类型;即对象
}

const p: ArrType = ['nba',123]// ts报错提示:(123)不能将类型“number”分配给类型“string”。
const p1: ArrType1 = ['james']// ts报错提示:不能将类型“string[]”分配给类型“ArrType1”。类型“string[]”中缺少类型“string”的索引签名。
const obj: ArrType1 = {
  name:'age',
  age:19// ts报错提示:不能将类型“number”分配给类型“string”。
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 什么是索引签名呢?
    • 有的时候,你不能提前知道一个类型里的所有属性的名字,但是你知道这些值的特征;
    • 这种情况,你就可以用一个索引签名 (index signature) 来描述可能的值的类型;
interface ICollection {
  [index: number]: any
  length: number
}

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

const tuple: [string, number, number] = ['kobe', 45, 1.96]
const array: string[] = ['nba','cba','ncaa']
logCollection(tuple)
logCollection("james")
console.log(array)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 一个索引签名的属性类型必须是 string 或者是 number【属性类型只能是string类型或number类型的其中一种;不可使用联合类型】。[属性: 属性类型]: 返回类型
  • 虽然 TypeScript 可以同时支持 string 和 number 类型,但数字索引的返回类型一定要是字符索引返回类型的子类型;(了解)
interface ICollection {
  [index: number]: string | number//数字索引的返回类型
  [key: string]: any  //字符索引返回类型
}
const arr: ICollection = ['nba']
console.log(arr[0],arr['0'])
1
2
3
4
5
6

# 13、接口继承

  • 接口和类一样是可以进行继承的,也是使用extends关键字:
    • 并且我们会发现,接口是支持多继承的(类不支持多继承)
interface Person {
  name: string
  eating: () => void
}

interface Animal {
  running: () => void
}

interface Student extends Person, Animal {// 多继承
  sno: number
}

const stu: Student = {
  sno: 119,
  name: 'kobe',
  eating() {},
  running() {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 14、接口被类实现 implements

  • 接口定义后,也是可以被类实现的
    • 如果被一个类实现,那么在之后需要传入接口的地方,都可以将这个类传入;
    • 这就是面向接口开发;
interface ISwim {
  name: string
  swimming: () => void
}

interface IRun {
  running: () => void
}

class Person implements ISwim, IRun {//接口定义后,也是可以被类实现的
  constructor(public name: string) {}
  swimming() {
    console.log(this.name,'swimming')
  }
  running(){
    console.log(this.name,'running')
  }
}

function swim(swimmer: ISwim) {
  swimmer.swimming()
}

const p = new Person('kobe')
swim(p)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 15、严格的字面量赋值检测

  • 对于对象的字面量赋值,在TypeScript中有一个非常有意思的现象:
    • 对于直接 作为函数的参数传入 或 对象字面量创建并赋值 的对象字面量,且函数的参数 或 接收对象的变量 有具体规定的类型,该情况会进行严格的字面量赋值检测【对象字面量必须遵守具体规定的类型】 具体理解可看下面案例二的情况二(报错)
    • 对于已经提前创建好的对象,该对象 作为函数的参数传入 或 直接赋值给另一个变量 (且函数的参数及变量 有具体规定的类型) 这里不会进行严格的字面量赋值检测 (因为对象在刚创建时是新鲜的;当作为参数传入,或者赋值给另一个变量时,这个时候,该对象已经不是新鲜的了;【而只有新鲜的情况,才会进行严格的字面量赋值检测】) 具体理解可看下面案例二的情况一(不报错)
//案例一
// 1.1:该情况ts报错(新鲜的)  
interface IPerson {
  name: string
  eating: () => void
}

const p: IPerson ={
  name:'kobe',
  age:18,// ts报错提示: 不能将类型“{ name: string; age: number; eating(): void; }”分配给类型“IPerson”。
  eating() {
    console.log(this.name,'eating')
  }
}

//1.2:这种情况ts不报错 (不新鲜的)
const obj = {
  name:'james',
  age:38,
  eating() {
    console.log(this.name,'eating')
  }
}

const p1: IPerson = obj


//案例二
function printInfo(info: IPerson) {
  console.log(info.name)
}

const o = {
  name:'rose',
  age:33,
  eating() {}
}

printInfo(o)//该情况不报错 (不新鲜的)-> 情况一

printInfo({//该情况报错(新鲜的)-> 情况二
  name:'rose',
  age:33,//类型“{ name: string; age: number; eating(): void; }”的参数不能赋给类型“IPerson”的参数。 对象文字可以只指定已知属性,并且“age”不在类型“IPerson”中。
  eating() {}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# 16、为什么会出现这种情况呢

  • 这里我引入TypeScript成员在GitHub的issue中的回答:

1.png

  • 简单对上面的英文进行翻译解释:
    • 每个对象字面量最初都被认为是“新鲜的(fresh)”。
    • 当一个新的对象字面量分配给一个变量或传递给一个非空目标类型的参数时,对象字面量指定目标类型中不存在的属性是错误的。
    • 当类型断言或对象字面量的类型扩大时,新鲜度会消失。

# 17、TypeScript枚举类型

  • 枚举类型是为数不多的TypeScript特性有的特性之一:
    • 枚举其实就是将一组可能出现的值,一个个列举出来,定义在一个类型中,这个类型就是枚举类型;
    • 枚举允许开发者定义一组命名常量,常量可以是数字、字符串类型;
enum Direction {
  LEFT,  //0
  RIGHT, //1
  TOP, //2
  BOTTOM //3
}

function turnDirection(direction: Direction) {
  switch(direction) {
    case Direction.LEFT:
      console.log('转向左边~')
      break
    case Direction.RIGHT:
      console.log('转向右边~')
      break
    case Direction.TOP:
      console.log('转向上边~')
      break
    case Direction.BOTTOM:
      console.log('转向下边~')
      break
    default:
      const myDirection: never = direction
  }
}

console.log(Direction.LEFT) //0
console.log(Direction.TOP) //2
turnDirection(0)//转向左边~
turnDirection(3)//转向下边~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 18、枚举类型的值

  • 枚举类型默认是有值的,比如上面的枚举,默认值是这样的:
enum Direction {
  LEFT = 0,
  RIGHT = 1,
  TOP = 2,
  BOTTOM = 3
}
1
2
3
4
5
6
  • 当然,我们也可以给枚举其他值:
    • 这个时候会从100进行递增;
enum Direction {
  LEFT = 100,
  RIGHT, //101
  TOP, //102
  BOTTOM //103
}

enum Direction {
  LEFT, //0
  RIGHT, //1
  TOP = 52,
  BOTTOM //53
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 我们也可以给他们赋值其他的类型:
enum Direction {
  LEFT,
  RIGHT,
  TOP = 'TOP',
  BOTTOM = 'BOTTOM',
  // CENTER // ts报错提示: 枚举成员必须具有初始化表达式。
  CENTER = 1 
}
1
2
3
4
5
6
7
8
最后更新时间: 2022/11/01, 11:56:41
彩虹
周杰伦