在上一篇文章中提到,我们可以将不同类别当中的共同属性或方法,提取出来放在 parent 类别当中,然後透过继承的方式,实现这些属性或方法,同时也可以加入额外的属性或方法。
以上次提到例子来说,BaseballPlayer
是一个 parent 类别,包含了 name
属性和 hit
方法
class BaseballPlayer {
name: string
constructor(name: string) {
this.name = name
}
hit() {
console.log(`${this.name} can hit`)
}
}
接着,我们建立 Shortstop
类别来继承 BaseballPlayer
class Shortstop extends BaseballPlayer {
run() {
console.log(`${this.name} can run`)
}
}
当然我们也可以建立另外一个 Outfielder
类别来继承 BaseballPlayer
,创造出更多不同类型的 baseball players
class Outfielder extends BaseballPlayer {
run() {
console.log(`${this.name} can run very fast`)
}
}
最後,我们就可以实际创造出类别的实例,并呼叫其方法
const lindor = new Shortstop('lindor')
const betts = new Outfielder('betts')
lindor.hit() // lindor can hit
lindor.run() // lindor can run
betts.hit() // betts can hit
betts.run() // betts can run very fast
这时候,隔壁棚传来新的需求,想要建立一个同样可以实作出 name
属性和 hit
方法的网球选手,於是就直接让 TennisPlayer
去继承 BaseballPlayer
class TennisPlayer extends BaseballPlayer {
serve() {
console.log(`${this.name} can serve`)
}
}
const federer = new Golfer('federer')
federer.hit() // federer can hit
federer.walk() // federer can serve
网球选手继承棒球选手?虽然实作出来的结果如预期,但是看起来就非常的奇怪,而且没有逻辑。如果今天我们继续充实 BaseballPlayer
类别,譬如加入 pitch
方法,变成
class BaseballPlayer {
name: string
constructor(name: string) {
this.name = name
}
hit() {
console.log(`${this.name} can hit`)
}
pitch() {
console.log(`${this.name} can pitch the ball`)
}
}
结果就会发现网球选手 federer 也会开始投球了
const federer = new Golfer('federer')
federer.pitch() // federer can pitch
虽然我们可以任意的分类、抽取属性和方法建立 parent 类别,然後任意的继承某个类别来取得需要的属性和方法,但这样的「整理方式」,最终只会造成无限的混乱和错误发生。
所以通常在建立 parent 类别和继承的时候,会遵循着「A 是 B 的一种」(is-a)的规则,譬如
这样一来,就能有逻辑的模拟真实世界的状况,也不会有意外的错误发生。
现在我们知道 TennisPlayer
不应该直接继承 BaseballPlayer
,不过这看起来好像也不是什麽大问题,只要直接建立两个完全独立的类别,然後分别实作各种方法,像是 hit
, pitch
, serve
等等。
但是我们还是希望可以稍微整理一下,让这个共同的方法在某种程度上被「抽取出来」或「规范」,并在未来建立其他新的类别的时候可以被使用。
接下来我们来看看几种不同的实作方式:
class Athlete {
name: string
constructor(name: string) {
this.name = name
}
hit() {
console.log(`${this.name} can hit`)
}
}
class BaseballPlayer extends Athlete {
pitch() {
console.log(`${this.name} can pitch the ball`)
}
}
class TennisPlayer extends Athlete {
serve() {
console.log(`${this.name} can serve`)
}
}
这里我们建立了一个 Athlete
类别,并让 BaseballPlayer
和 TennisPlayer
分别继承他的属性和方法,如此一来,baseball player 和 tennis player 都可使用同样的 hit
方法
const jeter = new BaseballPlayer('jeter')
const federer = new TennisPlayer('federer')
const someone = new Athlete('someone')
jeter.hit() // jeter can hit
federer.hit() // federer can hit
someone.hit() // someone can hit
不过这时候又发现了几个小问题,一个是虽然我们希望 baseball player 和 tennis player 都可以使用 hit
,但是两者实际实作 hit
的方式和细节可能不太一样,譬如我们希望变成
jeter.hit() // jeter can hit baseball
federer.hit() // federer can hit tennis
另外一个是,也许我们不需要为 Athlete
建立实例 (e.g., someone)。我们期待各种运动选手都应该来自於各种选手的类别,然後这些类别共同继承Athlete
。
为了解决刚才提到的问题,所以现在我们换个方式做。相对於建立一个 parent 类别,这里我们建立一个抽象类别 Athlete
。
abstract class Athlete {
name: string
constructor(name: string) {
this.name = name
}
abstract hit(): void;
}
class BaseballPlayer extends Athlete {
hit() {
console.log(`${this.name} can hit baseball`)
}
pitch() {
console.log(`${this.name} can pitch the ball`)
}
}
class TennisPlayer extends Athlete {
hit() {
console.log(`${this.name} can hit tennis`)
}
serve() {
console.log(`${this.name} can serve`)
}
}
跟刚刚不一样的地方是,我们无法透过抽象类别来建立实例,另一方面,我们只在这个抽象类别定义了 hit
这个方法的存在,但是没有定义 hit
的实作细节。所以 baseball player 和 tennis player 可以有同样的 hit
方法但是有不同的结果
const jeter = new BaseballPlayer('jeter')
const federer = new TennisPlayer('federer')
const someone = new Athlete('someone') // error
jeter.hit() // jeter can hit baseball
federer.hit() // federer can hit tennis
除了抽象类别之外,我们还可以使用介面 (interface)。介面本身定义了 hit
方法的存在,但是没有定义它的实作方式。hit
实作的方式被定义在使用 (implements) 该介面的类别当中
interface Hit {
hit(): void;
}
class BaseballPlayer implements Hit {
name: string
constructor(name: string) {
this.name = name
}
hit() {
console.log(`${this.name} can hit baseball`)
}
pitch() {
console.log(`${this.name} can pitch the ball`)
}
}
class TennisPlayer implements Hit {
name: string
constructor(name: string) {
this.name = name
}
hit() {
console.log(`${this.name} can hit tennis`)
}
serve() {
console.log(`${this.name} can serve`)
}
}
和抽象类别一样,介面无法建立实例
const jeter = new BaseballPlayer('jeter')
const federer = new TennisPlayer('federer')
const someone = new Hit() // error
jeter.hit() // jeter can hit baseball
federer.hit() // federer can hit tennis
不过,究竟什麽是「抽象类别」和「介面」,就让我们下一篇文章多谈一些吧!
本篇文章同步发表在 HKT 线上教室 部落格,线上影音教学课程已上架至 Udemy 和 Youtu...
EADDRINUSE 表示你使用的Port被其他的Application占用,你可以把占用的appl...
这个插件就如同名称一样,是专门寄信使用的(恩对,介绍就这样而已)。 准备 在开始使用之前要先做好前置...
什麽是单向绑定什麽是双向绑定?简单来说一个只有单方面的传送,另一个则是可以来回传,wow讲完了,今天...
阿嬷都看得懂的 JQuery 怎麽写 昨天我们聊到怎麽使用 document 这个咒语,让神灯精灵帮...