likes
comments
collection
share

手牵手带你掌握TS的面向对象:类的使用,抽象类,鸭子类型,对象类型,索引签名,接口,严格字面量检测,枚举

作者站长头像
站长
· 阅读数 5

概述

上篇文章我们讲述了TS中的特有的一些语法细节类知识,本篇博客将讲述TS中类的使用、抽象类、鸭子类型、对象类型、索引签名、接口、严格字面量检测、枚举等知识。闲言少叙,让我们开始吧

TS中类的使用

类的基本使用

我们来定义一个 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("Taylor", 18)
const p2 = new Person("Kobe", 30)

console.log(p1.name, p2.age)

export {} 

类的继承

面向对象的其中一大特性就是继承,继承不仅仅可以减少我们的代码量,也是多态的使用前提。我们使用extends关键字来实现继承子类中使用 super来访问父类。

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")
  }
}

class Student extends Person {
  sno: number,
  constructor(name: string, age: number, sno: number) {
    super(name, age)
	this.sno = sno
  }
  studying() {
    console.log(this.name + "is studying")
  }
}

export {}

类的成员修饰符

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

  • public修饰的是在任何地方可见、公有的属性或方法默认编写的属性就是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()  // 报错,因为方法eating是private

// 子类中是否可以访问
class Student extends Person {
  constructor(name: string, age: number) {
    super(name, age)
  }

  studying() {
    console.log("在学习", this.name)
  }
}

const stu = new Student("Lee", 18)
// console.log(stu.name)  // 报错,因为name是protected

export {}

只读属性readonly修饰符

如果有一个属性我们不希望外界可以任意的修改,只希望确定值后直接使用, 那么可以使用readonly 手牵手带你掌握TS的面向对象:类的使用,抽象类,鸭子类型,对象类型,索引签名,接口,严格字面量检测,枚举

getters和setters

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

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("Lee", 100)
p.name = "Kobe"  // 调用的是setter方法
console.log(p.name) // 调用的是getter方法

p.age = -10
console.log(p.age)

export {}

参数属性

TypeScript提供了特殊的语法,可以把一个构造函数参数转成同名同值类属性,这些就被称为参数属性( parameter properties); 可以通过在构造函数参数前添加一个可见性修饰符public private protected或者readonly 来创建参数属性 ,最后这些类属性字段也会得到这些修饰符

class Person {
  // 语法糖
  // name, _age, height自动变成类属性
  constructor(public name: string, private _age: number, readonly height: number) {
  }

  running() {
    console.log(this._age, "eating")
  }
}

const p = new Person("Lee", 18, 1.88)
console.log(p.name, p.height)

// p.height = 1.98

export {}

抽象类和抽象方法

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

什么是抽象方法 ? 在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 {
  constructor(public bottom: number, public height: number) {
    super()
  }
  getArea() {
    return this.bottom * this.height * 0.5
  }
}

// 通用的函数
function calcArea(shape: Shape) {
  return shape.getArea()
}

calcArea(new Rectangle(10, 20))
calcArea(new Circle(5))
calcArea(new Triangle(3, 6))

// 在Java中会报错: 不允许
calcArea({ getArea: function() {} })

// 抽象类不能被实例化
// calcArea(new Shape())
// calcArea(100)
// calcArea("abc")

export {}

类型检测:鸭子类型

TypeScript对于类型检测的时候使用的鸭子类型,那什么是鸭子类型呢?

鸭子类型: 如果一只鸟, 看起来像鸭子, 走起来像鸭子, 游起来也像鸭子, 那么你可以认为它就是一只鸭子

即鸭子类型, 只关心属性和行为, 不关心你具体是不是对应的类型 是不是还是有些迷糊,那一起来看段代码吧

class Person {
  constructor(public name: string, public age: number) {}

  running() {}
}

class Monkey {
  constructor(public name: string, public age: number) {}
  running() {}
}

function printPerson(p: Person) {
  console.log(p.name, p.age)
}

printPerson(new Person("James", 18))
// printPerson("abc")
printPerson({name: "Kobe", age: 30, running: function() {}})

// 下面两行代码都没有问题
// 理解一下,这就是鸭子类型的应用体现
printPerson(new Monkey("旺财", 3))
const person: Person = new Monkey("果汁", 5)

export {}

类的类型

类本身也可以作为一种数据类型。 类的作用:

  1. 可以创建类对应的实例对象
  2. 类本身可以作为实例的类型
  3. 类也可以当作一个构造签名的函数
class Person {}

const name: string = "aaa"
// 1.创建类对应的实例对象
const p: Person = new Person()

// 2.作为实例的类型
function printPerson(p: Person) {}

function factory(ctor: new () => void) { }
// 3.当作一个构造签名的函数
factory(Person)

export {}

对象类型的属性操作符

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

可选属性(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: "Kobe",
  age: 18
}

// p.age = 30

const kun:IKun = {
  name: 'kun',
  slogan: 'rap'
}

// kun.slogan = '2.5 years'

export {}

索引签名

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

基本使用

需要注意的是:一个索引签名的属性类型必须是string或number。 虽然TS可以同时支持string和number类型,但数字类型的返回类型一定要是字符串索引返回类型的子类型

interface ICollection {
  // 索引签名
  // 返回值类型的目的是告知通过索引去获取到的值是什么类型
  [index: string]: number
  // 下方定义的属性也要符合上方定义的索引签名的规则
  length: number
}

const names: number[] = [111, 222, 333]
console.log(names['0'])
// 通过数字类型访问索引时, 最终都是转化成string类型访问
console.log(names[1])
console.log(names[2])


function iteratorCollection(collection: ICollection) {
  console.log(collection['0'])
  console.log(collection[1])
}

// iteratorCollection(names)
// const tuple: [string, string] = ["coder", "18"]
// iteratorCollection(tuple)

iteratorCollection({ name: 111, age: 18, length: 10 })

export {}  

索引签名索引的类型问题

interface IIndexType {
  // 返回值类型的目的是告知通过索引去获取到的值是什么类型
  // [index: number]: string
  // [index: string]: any
  [index: string]: string 
}

// 索引签名: [index: number]: string
// const names: IIndexType = ["abc", "cba", "nba"]

// 索引签名: [index: string]: any: 没有报错
// 1.索引要求必须是字符串类型 names[0] => names["0"]
// const names: IIndexType = ["abc", "cba", "nba"]

// 索引签名: [index: string]: string: 会报错
// 因为通过字符串索引拿到的值不一定是字符串,例如names["forEach"]返回的是函数类型
// 2.严格字面量赋值检测: ["abc", "cba", "nba"] => Array实例 => names[0] names.forEach
// const names: IIndexType = ["abc", "cba", "nba"]
// names["forEach"] => function
// names["map/filter"] => function

export {}

两个索引签名

一些tips:

  1. 要求一: 数字类型索引的类型, 必须是字符串类型索引的类型的子类型

    • 结论: 数字类型必须是比如字符串类型更加确定的类型(需要是字符串类型的子类型)
    • 原因: 所有的数字类型都是会转成字符串类型去对象中获取内容
    • 数字0: number|string, 当我们是一个数字的时候, 要满足通过number去拿到的内容, 不会和string拿到的结果矛盾
    • 字符串"0": string
  2. 要求二: 如果索引签名中有定义其他属性, 其他属性返回的类型, 必须符合string类型返回的属性

interface IIndexType {
  // 两个索引类型的写法
  [index: number]: string
  [key: string]: any

  // 根据要求一,下面的写法错误
  // [index: number]: number|string
  // [key: string]: string

  // 要求二: 如果索引签名中有定义其他属性, 其他属性返回的类型, 必须符合string类型返回的属性
  // [index: number]: string
  // [key: string]: number|string

  // aaa: string
  // bbb: boolean 错误的类型
}

const names: IIndexType = ["abc", "cba", "nba"]
const item1 = names[0]
const forEachFn = names["forEach"]

names["aaa"]

export {}

TS接口

接口的继承 extends

接口和类一样,是可以进行继承的,也是使用extends关键字。并且,接口支持多继承

接口的优势:

  1. 可以从其他的接口中继承过来属性
  2. 减少了相同代码的重复编写
  3. 如果使用第三库, 给我们定义了一些属性
    • 自定义一个接口, 同时你希望自定义接口拥有第三方某一个类型中所有的属性,可以使用继承来完成
interface IPerson {
  name: string
  age: number
}

interface IKun extends IPerson {
  slogan: string
}

const ikun: IKun = {
  name: "Kobe",
  age: 18,
  slogan: "rap"
}

console.log(ikun.slogan)

export {}

接口的实现 implements

接口定义后,也可以被类实现使用implements关键字

interface IKun {
  name: string
  age: number
  slogan: string

  playBasketball: () => void
}

interface IRun {
  running: () => void
}


const ikun: IKun = {
  name: "kun",
  age: 18,
  slogan: "rap",
  playBasketball: function() {}
}

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

  playBasketball() {
    console.log("I can sing, dance & rap")
  }

  running() {
	console.log("Run!")
  }
}

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

export {}

抽象类和接口的区别

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

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

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

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

  1. 抽象类是对事物的抽象,表达的是 is a 的关系。eg: 猫是一种动物(动物就可以定义为一个抽象类)
  2. 接口是对行为的抽象,表达的是 has a 的关系。猫可以跑(可以定义一个单独的接口)、爬树(也可以定义一个单独的接口)

严格的字面量赋值检测

对于对象的字面量赋值,在TS中有一个非常有意思的现象。

interface IPerson {
  name: string
  age: number
}

// 1.奇怪的现象一: 
// 定义info, 类型是IPerson类型
const obj = {
  name: "why",
  age: 18,

  // 多了一个height属性
  height: 1.88
}
// 以下代码不会报错
const info: IPerson = obj

// 2.奇怪的现象二:
function printPerson(person: IPerson) {

}
const kobe = { name: "kobe", age: 30, height: 1.98 }
// 以下代码也不会报错
printPerson(kobe)


// 解释现象
// 第一次创建的对象字面量, 称之为fresh(新鲜的)
// 对于新鲜的字面量, 会进行严格的类型检测. 必须完全满足类型的要求(不能有多余的属性)
const obj2 = {
  name: "why",
  age: 18,

  height: 1.88
}
// 以下代码也不会报错
const p: IPerson = obj2

export {}

这里贴一个TS成员在github上的回答 手牵手带你掌握TS的面向对象:类的使用,抽象类,鸭子类型,对象类型,索引签名,接口,严格字面量检测,枚举

枚举类型

枚举类型是TypeScript 为数不多的特性之一

  • 枚举其实就是将一组可能出现的值,一个个列举出,定义在一个类型中,这个类型就是枚举类型
  • 枚举允许开发者定义一组命名常量,可以是数字、字符串类型
  • 枚举类型默认是有值的,从0开始,也可以指定从固定数值开始递增
  • 也可以赋值给其他类型,如字符串等
// 定义枚举类型
// enum Direction {
//   LEFT = 0,
//   RIGHT = 1
// }

// enum Direction {
//   LEFT = 100,
//   RIGHT
// }

enum Direction {
  LEFT = "LEFT",
  RIGHT = "RIGHT"
  UP = 3
  DOWN = 4
}

enum Operation {
  Read = 1 << 0,
  Write = 1 << 1,
  foo = 1 << 2
}

const d1: Direction = Direction.LEFT

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

export {}

总结

相信大家通过本篇文章的叙述都对TS面向对象的知识有了初步的了解,但不知道大家注意到没有,直到现在为止,我们无论对于类的成员属性类型定义还是普通变量的类型定义,亦或是函数参数和返回值的类型定义,用的不一定是已经确定的类型,并且得到的也不一定是确定的类型?!不知道说到这里大家有没有疑惑?举个栗子:

// 下面的代码在ts中会报错
function sum(num1: number|string, num2: number|string) {
    return num1 + num2
}

如上例,如果我们给一个函数参数类型用联合类型声明,那它的参数就是一个变化的类型,我们的本意是想,我们输入number类型,结果返回number类型,我们输入string类型,结果返回string类型,但目前我们好像只能通过函数重载实现。那有没有其他方法可以使得我们输入的类型由自己自行确定,输出的类型也得到确定的类型呢?这就不得不引出我们的泛型了!下篇文章就让我们一起来了解一下TS中泛型编程哪些事吧!