手牵手带你掌握TS的面向对象:类的使用,抽象类,鸭子类型,对象类型,索引签名,接口,严格字面量检测,枚举
概述
上篇文章我们讲述了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
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 {}
类的类型
类本身也可以作为一种数据类型。 类的作用:
- 可以创建类对应的实例对象
- 类本身可以作为实例的类型
- 类也可以当作一个构造签名的函数
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:
-
要求一: 数字类型索引的类型, 必须是字符串类型索引的类型的子类型
- 结论: 数字类型必须是比如字符串类型更加确定的类型(需要是字符串类型的子类型)
- 原因: 所有的数字类型都是会转成字符串类型去对象中获取内容
- 数字0: number|string, 当我们是一个数字的时候, 要满足通过number去拿到的内容, 不会和string拿到的结果矛盾
- 字符串"0": string
-
要求二: 如果索引签名中有定义其他属性, 其他属性返回的类型, 必须符合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关键字。并且,接口支持多继承。
接口的优势:
- 可以从其他的接口中继承过来属性
- 减少了相同代码的重复编写
- 如果使用第三库, 给我们定义了一些属性
- 自定义一个接口, 同时你希望自定义接口拥有第三方某一个类型中所有的属性,可以使用继承来完成
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 {}
抽象类和接口的区别
抽象类在很大程度上和接口有些相似:都可以在其中定义一个方法,让子类或者实现类来实现对应的方法。
那抽象类和接口有什么区别呢?
- 抽象类是事物的抽象,抽象类用来捕捉子类的通用特性,接口通常是一些行为的描述。
- 抽象类通常用于一系列关系紧密的类之间,接口只是用来描述一个类应该具有什么行为
- 接口可以被多层实现,而抽象类只能单一继承
- 抽象类中可以有实现体,而接口中只能有函数的声明
通常我们这样来描述类和抽象类、接口之间的关系:
- 抽象类是对事物的抽象,表达的是
is a
的关系。eg: 猫是一种动物(动物就可以定义为一个抽象类) - 接口是对行为的抽象,表达的是
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上的回答
枚举类型
枚举类型是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中泛型编程哪些事吧!
转载自:https://juejin.cn/post/7358289528551981092