基本用法
ts 中类的写法与 js 中基本一致,区别暂时理解为只是在 constructor
函数前,需要先定义这个类的公共属性,声明类型。并且在严格模式下属性都需要初始化。
可以是直接赋值,此时类型可以通过推导得到:
typescript
class Person {
name = 'Jay'
age = 20
}
或通过 constructor
赋值:
typescript
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
继承(extends)
继承 和多态 还有封装 属于面向对象的三大特性,且继承为后面介绍的多态的前提。跟es6 里类的继承几乎一致,比如 Person 类继承了 Animal 类,那么我们就称 Person 类为派生类 (子类 ),Animal 类为基类 (超类 ,也可以称为父类)。
- 类之间的继承关系使用的是
extends
关键词; - 子类如果定义了构造函数
constructor
,必须调用super()
(相当于执行了父类的构造函数),且需要写在前面,之后才能在子类的constructor
中使用this
; - 可以在子类中重写父类的方法。也可以通过
super
调用父类的方法,且位置随意。
代码例子:
typescript
// 定义一个基类
class Animal {
eyesNumber: number
constructor(num: number) {
this.eyesNumber = num
}
run(speed: string) {
console.log(`跑得${speed}`)
}
}
// 定义一个派生类
class Person extends Animal {
name: string
constructor(name: string, num: number = 2) {
// 使用 super 调用父类的构造函数
super(num)
this.name = name
}
run() {
console.log('我是子类的方法,可以写在 super 前面')
// 调用父类中的方法
super.run('快')
}
}
const person = new Person('Jay')
console.log(person.eyesNumber)
console.log(person.name)
person.run()
打印结果如下:
多态
定义:父类型的引用指向了子类型的对象,不同类型的对象(有继承关系)针对相同的方法,有不同的表现。 如下例子中,先定义一个父类 Father
和两个子类Son
、Daughter
:
typescript
// 定义父类
class Father {
name: string
constructor(n: string) {
this.name = n
}
work(days: number = 5) {
console.log(`${this.name}一周工作${days}天`)
}
}
// 定义子类
class Son extends Father {
constructor(n: string) {
super(n)
}
work(days: number = 3) {
console.log(`${this.name}是男孩子,一周需要工作${days}天`)
}
}
// 定义子类
class Daughter extends Father {
constructor(n: string) {
super(n)
}
work(days: number = 1) {
console.log(`${this.name}是女孩子,一周只需要工作${days}天`)
}
}
将上述 3 个类实例化,因为 Son 和 Daughter 是继承自 Father 类的,所以我们可以将 Son 或 Daughter 的实例对象赋值给类型为 Father 的变量,然后分别调用 work
方法:
typescript
const chaim: Father = new Father('Chaim')
chaim.work()
const jay: Father = new Son('Jay')
jay.work()
const linda: Father = new Daughter('Linda')
linda.work()
输出如下图:
发现同样是 work
方法,但因为 jay
(或 linda
) 虽然定义时类型是 Father,但本质上类型是 Son(或 Daughter),所以执行的是 Son(或 Daughter) 的 work
,这就是多态。
另外,如果子类型没有增加其它方法, 也可以让子类型引用指向父类型的实例(因为实例都是只有 name
属性和 work
方法的对象):
typescript
const jay: Son = new Father('Jay')
jay.work() // Jay一周工作5天
成员修饰符
主要是描述类中的成员(属性,构造函数,方法)的可访问性。
- public 公共的
类中成员默认的修饰符,代表是公共的,任何位置都可以访问类中的成员:
typescript
class Singer {
public name: string
public constructor(name: string) {
this.name = name
}
public releaseAlbum() {
console.log(`一个歌手一年发一张专辑不过分吧,啊?${this.name}`)
}
}
const singer = new Singer('Jay')
console.log(singer.name) // Jay
singer.releaseAlbum() // 一个歌手一年发一张专辑不过分吧,啊?Jay
- private 私有的
类中的成员如果使用 private
修饰,则外部无法访问与修改,包括子类。如果想在外部访问或修改私有属性,可以通过定义 getter 和 setter:
typescript
class Singer {
private _name: string // 私有属性的属性名按惯例以 _ 开头
constructor(name: string) {
this._name = name
}
get name() {
return this._name // 内部可以访问私有属性
}
set name(value) {
// 可以在这里做一些操作
this._name = value
}
}
const singer = new Singer('Jay')
console.log(singer.name) // Jay
singer.name = 'Chaim'
console.log(singer.name) // Chaim
如果 constructor
被 private
修饰,new Singer()
就会报错。
- protected 受保护的
类中的成员如果使用 protected
修饰,则外部无法访问,但是子类中是可以访问的
typescript
class Singer {
protected name: string
constructor(name: string) {
this.name = name
}
}
class Rapper extends Singer {
constructor(name: string) {
super(name)
}
sing() {
console.log(`${this.name} 唱了一首歌`)
}
}
const singer = new Singer('Jay')
console.log(singer.name) // 编辑器提示错误,属性 "name" 受保护,只能在类 "Singer" 及其子类中访问
const rapper = new Rapper('Hey')
rapper.sing() // Hey 唱了一首歌
- readonly 只读的
类中的成员如果使用 readonly 修饰,则该属性成员就不能在外部被修改了,在类的内部的一般方法里也不能修改,只能在类的构造函数中进行修改:
typescript
class Singer {
readonly name: string
constructor(name: string) {
this.name = name
}
}
const singer = new Singer('Jay')
注意:如果一个只读属性的类型是一个类,那么这个属性本身的值不能修改,但是它的属性是可以修改的。比如下例中 friend
属性是不能修改的,但 friend.name
可以:
typescript
class Singer {
name: string
readonly friend: Singer
constructor(name: string, friend?: Singer) {
this.name = name
if (friend)
this.friend = friend
}
}
const singer = new Singer('Jay', new Singer('Eson'))
singer.friend = new Singer('Jon') // 报错:无法分配到 "friend" ,因为它是只读属性
singer.friend.name = 'Ella' // 可以修改
参数属性(Parame Properties)
ts 提供了一种语法糖写法,即以上这些修饰符,可以直接对构造函数中的参数进行修饰,此时对应的属性会自动赋值,并且不用再特意在一开始时罗列出来:
typescript
class Singer {
// name: string // 构造函数中用了修饰符,这里无需再写
constructor(public name: string) {
// this.name = name // 当 new Singer('Jay') 时会自动将 'Jay' 赋值给 name
}
}
存取器
TypeScript 支持通过 getters/setters
来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
typescript
class PersonName {
constructor(public firstName: string, public lastName: string) {
this.firstName = firstName
this.lastName = lastName
}
get fullName() {
return this.firstName + this.lastName
}
set fullName(val) {
const nameArr = val.split(' ')
this.firstName = nameArr[0]
this.lastName = nameArr[1]
}
}
const personName: PersonName = new PersonName('Jay', 'Zhou')
console.log(personName.fullName) // JayZhou
personName.fullName = 'Chaim HL'
console.log(personName.fullName) // ChaimHL
如果把 set
部分的代码注释掉,则 fullName
就是个只读的属性,personName.fullName = 'Chaim HL'
就会报错。
静态属性 static
类中的成员如果使用 static 修饰,则是静态属性或静态方法,只能通过 类名.
的方式调用,注意 static 不能像其他修饰符那样在构造函数的参数中使用
typescript
class PersonName {
static firstName: string = 'Jay'
}
console.log(PersonName.firstName) // Jay
静态方法只能使用静态属性或方法:
typescript
class Test {
static num = 0
static fn() {
Test.bar() // bar
this.bar() // bar
console.log(Test.num) // 0
console.log(this.num) // 0
}
static bar() {
console.log('bar')
}
}
抽象类
abstract
关键字定义的类为抽象类,可以包含同样被 abstract
修饰的方法,也就是抽象方法(一般没有任何具体的实现),也可以包含抽象属性(很少定义抽象属性),和实例方法。抽象类不能被实例化,而是为了让子类进行实例化及实现内部的抽象方法。不同于接口,抽象类可以包含成员的实现细节。
typescript
abstract class Animal {
// abstract name: string = 'Animal' // 可以定义抽象属性,但一般很少这么做
// abstract eat(): void {} // 会报错,因为抽象方法不能有具体实现
abstract eat(): void // 抽象方法只能定义在抽象类中
run() {
console.log('这是实例方法')
}
}
// const animal = new Animal() // 报错:无法创建抽象类的实例
class Person extends Animal {
eat() {
console.log('子类必须实现抽象父类的抽象方法')
}
}
const person: Person = new Person()
person.eat() // 子类必须实现抽象父类的抽象方法
person.run() // 这是实例方法
类类型
下例中,定义了一个 Singer
类,和一个类型为 Singer
的对象 singer
。singer
的值不一定得是通过 new Singer
得到,也可以直接赋值一个对象,只要该对象里有 Singer
中定义的实例属性和方法即可:
typescript
class Singer {
name: string
constructor(name: string) {
this.name = name
}
sing() {
console.log('是谁在唱歌')
}
}
const singer: Singer = {
name: 'Jay',
sing() { }
}
注意,类是不存在下面这种写法的,类只是继承某个类或实现某些个接口:
鸭子类型(duck typing)
最后说说 ts 中类型检测的一个原则 ------ 鸭子类型,就是侧重于检查对象的属性和行为,当两个对象它们具有相同的属性和方法时,它们就会被认为属于相同的类,即"如果一只鸟走起来像鸭子,游起来像鸭子,叫起来像鸭子,那么它就是一只鸭子"。
typescript
class Person {
constructor(public name: string) { }
eat() { }
}
class Singer {
constructor(public name: string) { }
eat() { }
}
const p: Singer = new Person('Jay')
因为 Singer
类的属性和方法 Person
类也都拥有,所以可以将 Person
的实例赋值给类型为 Singer
的 p
。但如果 Singer
类还有个 run
方法,那么赋值就会报错。