三、TypeScript 面向对象

96 阅读10分钟

一、TypeScript类的使用

1. 认识类的使用

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

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
    }
    
    running() {
        console.log(this.name + "running")
    }
    
    eating() {
        console.log(this.name + "eating")
    }
}

3. 类的继承

  • 面向对象的其中一大特性就是继承,继承不仅仅可以减少我们的代码量,也是多态的使用前提
  • 使用 extends 关键字来实现继承,子类中使用 super 来访问父类
  • 看一下 Student 类继承自 Person
    • Student 类可以有自己的属性和方法,并且会继承 Person 的属性和方法
    • 在构造函数中,可以通过 super 来调用父类的构造方法,对父类中的属性进行初始化
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")
    }
}

4. 类的成员修饰符

  • 在 TypeScript 中,类的属性和方法支持三种修饰符:public、private、protected
    • public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的
    • private 修饰的是仅在同一类中可见、私有的属性或方法
    • protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法
  • public 是默认的修饰符,也是可以直接访问的,如下延时protected和private
class Person {
    protected name: string
    
    constructor(name: string) {
        this.name = name
    }
}

class Student extends Person {
    constructor(name: string) {
        super(name)
    }
    
    running() {
        console.log(this.name + "running")
    }
}
class Person {
    private name: string
    
    constructor(name: string) {
        this.name = name
    }
}

const p = new Person("ikun")
// console.log(p.name) 属性 ”name“ 为私有属性,只能在类 ”Person“ 中访问

5. 只读属性readonly

  • 如果有一个属性不希望外界可以任意修改,只希望确定值后直接使用,那么可以使用 readonly
class Person {
    readonly name: string
    constructor(name: string) {
        this.name = name
    }
}

const p = new Person("ikun")
// console.log(p.name) 只读报错

6. getters/setters

  • 私有属性是不能直接访问的,或某些属性想要监听它的获取(getter)和设置(setter)的过程,需要使用存取器
class Person {
    private _name: string
    set name(newVal) {
        this._name = newVal
    }
    get name() {
        return this._name
    }
    
    constructor(name: string) {
        this.name = name
    }
}

const p = new Person("ikun")
p.name = "haha"
console.log(p.name)

7. 参数属性(Parameter Properties)

  • TypeScript 提供了特殊的语法,可以把一个构造函数参数转化成一个同名同值的类属性
    • 这些被称为参数属性(parameter properties)
    • 可以通过在构造函数参数前添加一个可见性修饰符 public private protected 或者 readonly 来创建参数属性,最后这些类属性字段也会得到这些修饰符
class Person {
    constructor(public name: string, private _age: number) {}
    set age(newVal) {
        this._age = newVal
    }
    get age() {
        return this._age
    }
}

二、TypeScript中抽象类

1. 抽象类abstract

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

class Circle extends Shape {
  private r: number
  constructor(r: number) {
    super()
    this.r = r
  }
  getArea(): number {
    return this.r * this.r * 3.1415926
  }
}
class Rectangle extends Shape {
  private width: number
  private height: number
  constructor(width: number, height: number) {
    super()
    this.width = width
    this.height = height
  }
  getArea(): number {
    return this.width * this.height
  }
}

const circle = new Circle(10)
const rectangle = new Rectangle(20, 30)

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

2. 鸭子类型

  • TypeScript 对于类型检测的时候使用的是鸭子类型
    • 鸭子类型:如果一只鸟,走起来像鸭子,游起来像鸭子,看起来像鸭子,那么可以认为就是一只鸭子
  • 鸭子类型
    • 只关心属性和行为,不关心你具体是不是对应的类型

3. 类的类型

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

const p1: Person = new Person("ikun")
const p2: Person = {
    name: "ikun",
    running: function() {
        console.log(this.name + "running")
    }
}
  • 类的作用
    • 可以创建类对应的实例对象
    • 类本身可以作为这个实例的类型
    • 类也可以当中有一个构造签名的函数

三、TypeScript对象类型

1. 对象类型的属性修饰符(Property Modifiers)

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

const p: IPerson = {
    name: "ikun",
    height: 1.80
}

四、TypeScript接口补充

1. 接口继承

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

interface Animal {
    running: () => void
}

interface Student extends Person, Animal {
    age: number
}

const s: Student = {
    age: 17,
    name: "ikun",
    eating: function() {},
    running: function() {}
}

2. 接口的实现

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

class Person implements ISwim, IRun {
    swimming() {}
    running() {}
}

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

const p = new Person()
swim(p)

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

  • 抽象类在很大程度上和接口会有点类似
    • 都可以在其中定义一个方法,让子类或实现类来实现对应的方法
  • 那抽象类和接口有什么区别?
    • 抽象类是事物的抽象,抽象类用来捕捉子类的通用特性,接口通常是一些行为的描述
    • 抽象类通常用于一系列关系紧密的类之间,接口只是用来描述一个类应该具有什么行为
    • 接口可以被多层实现,而抽象类只能单一继承
    • 抽象类中可以有实现体,接口中只能有函数的声明
  • 通常这样描述类和抽象类、接口之间的关系
    • 抽象类是对事物的抽象,表达的是 is a 的关系。
      • 狗是一种动物(动物就可以定义成一个抽象类)
    • 接口是对行为的抽象,表达的是 has a 的关系。
      • 猫可以跑(可以定义一个单独的接口)、吃(可以定义一个单独的接口)的行为

4. 索引签名(Index Signatures)

  • 索引签名
    • 不能提前知道一个类型里所有属性的名字,但是知道这些值的特征
    • 可以用一个索引签名(index signatures)来描述可能得值的类型
  • 一个索引签名的属性类型必须是 string 或 number
    • TypeScript 可以同时支持 string 和 number 类型,但数字索引的返回类型一定要是字符索引返回类型的子类型
      • 原因:所有的数字类型都是会转成字符串类型去对象中获取内容
      • 数字 0:number | string,当使用数字 0 取值时,要满足通过 number 去拿到的内容,不能和使用 string '0' 取值拿到的结果矛盾
    • 如果索引签名中有定义其它属性,其它属性返回的类型,必须符合 string 类型返回的属性
interface ICollection {
    [index: number]: any
    length: number
}

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

const tuple: [string, number, number] = ["ikun", 17, 180]
const arr: string[] = ["a", "b", "c"]
logCollection(tuple)
console.log(arr)

五、特殊:严格字面量检测

1. 严格的字面量赋值检测

  • 对于对象的字面量赋值,在 TypeScript 中有一个有意思的现象
    • 每个对象字面量最初都被认为是”新鲜的(fresh)“
    • 当一个新的对象字面量分配给一个变量或传递给一个非空目标类型的参数时,对象字面量制定目标类型中不存在的属性是错误的
    • 当类型断言或对象字面量的类型扩大时,新鲜度就会消失
interface IPerson {
    name: string
    eating: () => void
}
// 对象字面量只能指定已知属性,并且 ”age“ 不在类型 ”IPerson“ 中
const p: IPerson = {
    name: "ikun",
    age: 17, // 标红
    eating: function() {}
}

function print(p: IPerson) {
    console.log(p.name)
}
// 对象字面量只能指定已知属性,并且 ”age“ 不在类型 ”IPerson“ 中
print({name: "ikun", age: 17}) // age: 17 标红

// 如下写法可以
const obj = {
  name: 'ikun',
  age: 17,
  eating: () => {},
}
const p: IPerson = obj

六、TypeScript枚举类型

1. TypeScript 枚举类型

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

2. 枚举类型的值

  • 枚举类型默认是有值的,比如上面的枚举,默认值是这样的
  • 当然,可以给枚举其它值
    • 这个时候会从 100 开始进行递增
  • 也可以赋值其它类型
enum Direction {
    LEFT = 0,
    RIGHT = 1,
    TOP = 2,
    BOTTOM = 3
}

enum Direction {
    LEFT = 100, // 100
    RIGHT, // 101
    TOP, // 102
    BOTTOM // 103
}

enum Direction {
    LEFT, // 0
    RIGHT, // 1
    TOP = "TOP", // TOP
    BOTTOM = "BOTTOM" // BOTTOM
}