TS 的类其实和 ES6 里的类差不多,只不过 TS 加多了一些功能。这篇文章会介绍 TS 类的常用功能与接口的对比,以及抽象类。
入门
还是先从基础(ES6 自带)的语法讲起吧,假设现在我们要定义 Github 的 GithubRepository 类。
class GithubRepository {
name: string
commits: number
constructor(name: string) {
this.name = name
this.commits = 0
}
remove():void {
console.log('Delete this repo')
}
rename(name: string):void {
this.name = name
}
}
let repo = new GithubRepository('My Repo')
GithubRepository 有仓库名字 (name),commit 的次数 (commits),还有删除仓库 (delete) 和重命名 (rename) 两个方法。这就是一个完整类的定义。
其中要注意的是一定要有 constructor 构造器,这是在 let repo = new GithubRepository('My Repo')
的时候用来创建临时对象的,在创建之后再将对象的内存地址赋值给 repo
的,所以无论要不要初始化类里的变量都要写 constructor。
继承
在 ES6 之前 JS 已经可以通过原型链实现类的继承功能了,ES6 其实加了个语法糖,而 TS 里的类继承和 ES6 是一样的。
现在微软收购了 Github 了嘛,那就假设他们家的仓库继承了 Github 的 GithubRepository 类吧。
class GithubRepository {
public name: string
public commits: number
constructor(name: string) {
this.name = name
this.commits = 0
}
remove():void {
console.log('Delete this repo')
}
rename(name: string):void {
this.name = name
}
}
class MicrosoftRepository extends Repository {
public logo: string = 'Microsoft'
constructor(name: string) {
super(name)
}
}
let repo = new MicrosoftRepository('My Repo')
现在假设微软的生成的 MicrosoftRepository 都要加上微软的 logo,所以 logo 放在 MicrosoftRepository 里。而在 constructor 里要调用 super
方法,还要将 name
传过去,作用相当于调用了 GithubRepository 类的 constructor。
现在变量 repo
就可以使用 GithubRepository 类里的方法,同时也具有 logo
属性了。
console.log(repo.name) // "My Repo"
repo.remove() // "Delete this repo"
repo.rename('Your Repo')
console.log(repo.name) // "Your Repo"
console.log(repo.logo) // "Microsoft"
作用域
public
现在我们定义 GithubRepository 里 name
和 commits
都是可以被外界访问的,所以这两个默认的作用域是 public
,也就说可以写成这样
class GithubRepository {
public name: string
public commits: number
...
}
let repo = new GithubRepository('My Repo')
console.log(repo.name) // 可以访问,"My Repo"
console.log(repo.commits) // 可以访问,0
private
假设现在有个变量 githubLogo
只能只属于 GithubRepoistory,而外界不能访问,当然微软的类也是不能访问的,这就要设置成 private 了。
class GithubRepository {
...
private githubLogo: string
...
}
class MicrosoftRepository extends GithubRepository {
public logo: string = 'Microsoft'
constructor(name: string) {
super(name)
console.log(this.commits) // 0
console.log(this.githubLogo) // 不能访问,报错
}
}
let repo = new GithubRepository('My Repo')
console.log(repo.githubLogo) // 出错,不能访问 githubLogo
protected
protected 的作用域就是只能在“本家族”里才能访问,别人都不能访问,有点像“家传秘方”的意思。
假设 Github 的 GithubRepository 有家传的推荐算法 recommend
,微软不想自己实现推荐算法,所以只好继承 GithubRepository 祖传的推荐算法喽。
class GithubRepository {
...
protected recommend(): void {
console.log('Recommend...')
}
}
class MicrosoftRepository extends GithubRepository {
public logo: string = 'Microsoft'
constructor(name: string) {
super(name)
this.recommend() // "Recommend..."
}
}
let repo = new MicrosoftRepository('My Repo')
repo.recommend() // 不能访问 recommend,报错
静态属性
静态属性(方法)用关键字 static
来表示。静态属性可以不用创建对象就可以访问该属性/调用该方法。
假设现在 GithubRepository 有一个方法是获取该仓库下截量的方法,用静态方法可以写成这样
class GithubRepository {
...
static getDownload(name: string): void {
console.log(`Repo ${name} download/month is ....`)
}
}
如果我们要查某个仓库的每月下载量就可以直接调用 getDownload
方法即可
GithubRepository.getDownload('MyRepo')
而不是创建一个 GithubRepository 去调用
(new GithubRepository()).getDownload('MyRepo')
静态属性类似,我们可以给 GithubRepository 加一个官网链接,这个链接变量设置为静态属性。
class GithubRepository {
...
public static url: string = 'https://github.com'
...
}
console.log(GithubRepository.url) // "https://github.com"
setter 与 getter
刚刚说过可以用 private 关键字使得某些属性不对外公开,这样我们就可以隐藏一些功能的实现了。比如说对不同浏览器的兼容等。
回到我们的例子,这个 GithubRepository 要对 Firefox,Chrome,IE 进行兼容,不同浏览器要去计算对应 Logo 的横坐标 X,这就可以使用 setter 与 getter 来完成了。
class GithubRepository {
...
private _logoPositionX: number = 0
set logoPositionX(rawX: number) {
browser = getBrowserName()
if (browser === 'IE') {
this._logoPositionX = rawX + 1
}
else if (browser = 'Chrome') {
this._logoPositionX = rawX + 2
}
else if (browser = 'Firefox') {
this._logoPositionX = rawX + 3
}
}
get logoPositionX(): number {
return this._logoPositionX
}
...
}
let repo = new MicrosoftRepository('My Repo')
repo.logoPositionX = 2
console.log(repo.logoPositionX) // "4"
上面的代码就将 _logoPositionX
隐藏了,每次设置新的位置时都会根据当前的浏览器进行再将计算,这就完成了浏览器的兼容,而这个兼容的操作外面是不知道的。外界只需要设置位置,和获取位置就可以了。
类与接口
其实这两个东西都是对创建对象的一种约束,不同的是类像是一个工厂,里面有很多功能,如设置属性的作用域,初始化对象等。接口更像说明书,只是说明这个对象应该有什么属性/方法,就没了。
使用代码可以看出他们有很大的不同。
interface Human {
name: string
gender: string
}
let jack:Human = {
name: 'Jack',
age: 18
}
下面是类的声明
class Human {
name: string
gender: string
constructor(name, gender) {
this.name = name
this.gender = gender
}
}
let jack = new Human('Jack', 18)
从上面可以看到接口的写法完全可以用类来替代,但是写类麻烦。简单来说两者的区别就是:
- 接口是类的低配版
- 类是接口的调配版
抽象类
说完接口与类的区别,我们来看看抽象类。抽象类的用法是在类的基础上可以不实现一些方法,而让子类去实现。
回到我们的例子,假设 Github 本来一直想实现一套仓库排名的算法,直到微软收购了还没有实现,所以这个重任就交给微软做了。
abstract class GithubRepository {
...
// 声明抽象方法
abstract sort(): void
}
class MicrosoftRepository extends GithubRepository {
...
// 实现抽象方法
sort(): void {
console.log('Sorting...')
}
}
let repo = new MicrosoftRepository('My Repo')
repo.sort() // "Sorting"
这里 GithubRepository 变成了抽象类,前面加 abstract
,里面就有一个还没实现的 sort
方法,所以前面也要加 abstract
。到了 MicrosoftRepository,他就一定要去实现 sort
方法了。
这里要注意的点是抽象类不能用来创建实例,想想看,如果可以创建实例,那未实现的方法调用怎么办呢?所以一定要有一个子类去实现那些未实现的方法,再用这个子类去创建实例。所以抽象类一般都作为“父亲类”,术语叫基类。他的功能是比一般的类要多的(可以声明未完成的方法)。
就像以前总有科学家提出 XXX 猜想,但就是自己不去实现或者自己不能实现,反而让那些苦逼大学生去实现。
抽象类与接口
这个抽象类怎么看起来和接口差不多呀。是差不多,但是又不能完全一样。
就像刚刚说的接口只是一份说明书,而抽象类就像工厂里的科学家,他提出很多猜想,同时也完成了很多实现,别的工厂(子类)就用继承他的思想去做自己的产品(创建实例)。
interface Human {
name: string
age: string
}
let jack = {
name: 'Jack',
age: 18
}
下面再看看抽象类的实现。
abstract class God {
constructor() { }
abstract createHuman(): void
}
abstract class Woman extends God {
createHuman():void {
console.log('XXOO') // :)
}
}
当然还能这么写
abstract class X {
name: string
gender: string
}
class Human extends X {
constructor(name: string, gender: string) {
super()
this.name = name
this.gender = gender
}
}
let jack = new Human('Jack', 'Male')
console.log(jack.name, jack.gender)