TypeScript面向对象的使用

166 阅读5分钟

类的使用

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

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

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

但是在封装某些业务的时候,类具有更强大封装性,所以我们也需要掌握它们。

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

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

类的定义

我们可以声明类的属性:在类的内部声明类的属性以及对应的类型

  • 如果类型没有声明,那么它们默认是any的;

  • 我们也可以给属性设置初始化值;

  • 在默认的strictPropertyInitialization模式下面我们的属性是必须初始化的,如果没有初始化,那么编译时就会报错;

    • ✓ 如果我们在strictPropertyInitialization模式下确实不希望给属性初始化,可以使用 name!: string语法;
  • 类可以有自己的构造函数constructor,当我们通过new关键字创建
    一个实例时,构造函数会被调用;

  • 构造函数不需要返回任何值,默认返回当前创建出来的实例;

image.png

我们发现在构造函数中没有初始化name,那么在定义的时候就需要定义为:!:

类中可以有自己的函数, 定义的函数称之为方法;

类的继承

image.png

和JS的语法一样,不过多介绍

成员修饰符

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

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

image.png

eating()方法变成私有的,外部就不可以访问到了,只有eating所在的类可以访问。

image.png

子类是不能访问父类私有的属性的,如果外界想要访问私有属性,需要对应私有类提供set、get方法,和java是一样的。

readonly只读属性

image.png

getter、setter

在前面一些私有属性我们是不能直接访问的,或者某些属性我们想要监听它的获取(getter)和设置(setter)的过程,这个时候我们可以使用存取器。

image.png

私有属性我们一般在前面会以_命名

class Person{
    private _name:string;
    
    constructor(name:string){
        this._name = name
    }
    
    set name(newValue:string){
        this._name=newValue
    }
    get name(){
        returh this._name
    }
}

const p = new Person("why")
p.name = "kobe" // 赋值
console.log(p.name) // 输出

因为一般情况下,我们不希望可以直接访问类的属性,而使用setter、getter,可以对属性的访问进行拦截操作。

class Person{
    private age:string;
    
    constructor(age:string){
        this.age = age
    }
    
    set age(newValue:string){
        // 拦截
        if(newValue>=0&&newValue<200){
            this._age=newValue
        }
    }
    get age(){
        returh this.age
    }
}

这样我们在进行年龄设置的时候,就可以防止直接修改类时,不符合逻辑值的操作。
通过setter拦截;

参数属性

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

  • 这些就被称为参数属性(parameter properties);
  • 你可以通过在构造函数参数前添加一个可见性修饰符 public private protected 或者 readonly 来创建参数属性,最后这些类属性字段也会得到这些修饰符;
class Person{
    age:number
    height:number
    
    // 语法糖:直接在这里写上修饰符,就等同于在 类和构造器中 同时定义了name:string
    constructor(public name:string,age:number,height:number){
        this.age=age
        this.height=height
    }
}

// 正常使用
const p = new Person("why",18,1.88)
p.name = "abc"

如果提供的private修饰符,那么外部想要访问,就得通过setter、getter了;

如果是readonly修饰符,则只能访问,不可以写入。

抽象类abstract

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

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

什么是 抽象方法? 在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
  }
}

没有方法体的就是抽象方法,前提是必须存在抽象类中,使用abstract声明抽象

由于上面的类都是继承Shape抽象类的,所以可以作为参数:

// 通用的函数:要求传入Shape类型,也就是shape的子类
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")

抽象类是不可以被实例化的!!!因为抽象类中存储抽象方法,而抽象方法是由子类实现的,所以我们应该实例化的是子类!!!

const shape1:Shape = new Circle(5)

那么这个就是父类的引用,指向了子类的对象,这就是多态。

TS的鸭子类型:

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({name: "kobe", age: 30, running: function() {}})

---------------------------------

printPerson(new Dog("旺财", 3))
const person: Person = new Dog("果汁", 5)

横线分割后的就是所谓的鸭子类型,printPerson明明要求传入的是一个Person类型,结果你传一个Dog类型的居然也算对,那是因为这两个类型中要求的属性是一样的;

但是在java中,如果类型不一样的话,是不允许传的,在TS中是可以的,只要在TS中定义的属性和行为相同。和传统的面向对象语言是有区别的。

类的类型

类本身也是可以作为一种数据类型的

class Person{
    
}

const name: string = "aaa"
const p: Person = new Person()

// 类可以作为一个类型
function printPerson(p:Person){}

export{}

这里p:PersonPerson就是一个类型

类的作用:

  • 可以创建类对应的实例对象
  • 类本身可以作为这个实例的类型
  • 类也可以当做一个有构造签名的函数
// factory()函数要求传入的参数是一个构造函数
function factory(ctor: new()=>void){}

const p:Person = new Person()
factory(Person)

类可以被当成一个有构造签名的函数

对象类型的属性修饰符

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

type IPerson = {
    // 属性 ?: 表示可选属性
    name?:string
    
    // readonly:只读属性
    readonly age:number
}

interface IKun{
    name:string
    slogan:string
}

const p:IPerson = {
    name:"why",
    age:18
}

image.png

索引签名

什么是索引签名呢?

  • 有的时候,你不能提前知道一个类型里的所有属性的名字,但是你知道这些值的特征;
  • 这种情况,你就可以用一个索引签名 (index signature) 来描述可能的值的类型;

一个索引签名的属性类型必须是 string 或者是 number。

  • 虽然 TypeScript 可以同时支持 string 和 number 类型,但数字索引的返回类型一定要是字符索引返回类型的子类型;(了解)
interface ICollection {
    // 索引签名:我能通过数字类型的索引,访问到string类型的值
    [index:number]:string
    length:number
}

const names:string[] = ["aaa","bbb","ccc"]

// 传入一个集合,并遍历
function iteratorCollection(collection:ICollection){}

// 这样我用索引也可以访问
iteratorCollection(names)

const tuple:[string,number] = ["why",18]
iteratorCollection(tuple)
  1. 索引签名的理解:
function getInfo(){
    const abc:any="aaa"
    return abc
}

const info = getInfo()
// 我们不知道这个info里面有啥属性,不过我们可以知道特点是什么

----------------------------------
可以使用索引签名:可以通过字符串索引,去获取到一个字符串值
interface InfoType{
    [key:string]:string
}

function getInfo():InfoType{
    const abc: any = "aaa"
    return abc
}

const info = getInfo()
// 这样就可以通过字符串索引,来访问到值了
console.log(info["name"], 也支持写法: info.age)
  1. 索引签名的理解
// 索引签名:根据数字类型的下标,取到一个字符串类型的值
interface ICollection{
    [index:number]:string
    length:number
}

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

const array = ["abc","cba","nba"]
const tuple: [string,string] = ["why","广州"]

printCollection(array)
printCollection(tuple)

export {}

可以通过索引下标拿到字符串类型的数值,并且这两个都有length属性。

索引签名是为了编写通用的接口

接口继承

接口和类一样是可以进行继承的,也是使用extends关键字:

  • 并且我们会发现,接口是支持多继承的(类不支持多继承)
  • 可以从其他接口中,继承过来属性
  • 减少了相同代码的重复编写
  • 我们可以自定义一个接口,并且可以拥有一些第三方库中的某些属性;
interface IPerson{
    name:string
    age:number
}

// 接口的继承
interface IKun extends IPerson{
    slogan:string
}

const ikun:IKun = {
    name:"aaa",
    age:18,
    slogan:"你干嘛"
}

export {}

接口的实现过程

类必须要实现接口,这样一来,类就具备了该接口所有的特性:const ikun2 = new Person()

  • 接口可以作为类型,进行限制
  • 接口可以被类实现
interface IKun {
  name: string
  age: number
  slogan: string

  playBasketball: () => void
}

interface IRun {
  running: () => void
}


const ikun: IKun = {
  name: "why",
  age: 18,
  slogan: "你干嘛!",
  playBasketball: function() {}
}

// 作用: 接口被类实现,并且实现多个接口
class Person implements IKun, IRun {
  name: string
  age: number
  slogan: string

  playBasketball() {
    
  }

  running() {

  }
}

const ikun2 = new Person()
const ikun3 = new Person()
const ikun4 = new Person()
console.log(ikun2.name, ikun2.age, ikun2.slogan)
ikun2.playBasketball()
ikun2.running()

image.png

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

image.png

枚举类型

enum Direction{
    UP,
    DOWN,
    LEFT,
    RIGHT
}

// d1是一个枚举类型的标识符,到底是什么类型
// 需要我们手动指定
const d1:Direction = Direction.UP

方向的移动经常会用到枚举类型:

// 定义枚举类型
enum Direction {
  LEFT,
  RIGHT
}

const d1: Direction = Direction.LEFT

function turnDirection(direction: Direction) {
  switch(direction) {
    case Direction.LEFT:
      console.log("角色向左移动一个格子")
      break
    case Direction.RIGHT:
      console.log("角色向右移动一个格子")
      break
  }
}

// 监听键盘的点击
turnDirection(Direction.LEFT)

image.png

枚举类型的值

image.png

使用位运算符的写法:了解即可,这个是计算机基础的知识,在写框架的时候,有些大佬喜欢这样写枚举:

enum Direction{
    Read= 1 << 0,
    Write = 1 << 1,
    Foo = 1 << 2
}

操作系统有提供计算器供我们参考:

image.png