對于傳統(tǒng)的 JavaScript 程序我們會使用
函數(shù)和基于原型的繼承來創(chuàng)建可重用的組件,但對于熟悉使用面向?qū)ο蠓绞降某绦騿T使用這些語法就有些棘手,因為他們用的是基于類的繼承并且對象是由類構(gòu)建出來的。 從 ECMAScript 2015,也就是 ES6 開始, JavaScript 程序員將能夠使用基于類的面向?qū)ο蟮姆绞健?使用 TypeScript,我們允許開發(fā)者現(xiàn)在就使用這些特性,并且編譯后的 JavaScript 可以在所有主流瀏覽器和平臺上運行,而不需要等到下個 JavaScript 版本。
基本示例
下面看一個使用類的例子:
/*
類的基本定義與使用
*/
class Greeter {
// 聲明屬性
message: string
// 構(gòu)造方法
constructor (message: string) {
this.message = message
}
// 一般方法
greet (): string {
return 'Hello ' + this.message
}
}
// 創(chuàng)建類的實例
const greeter = new Greeter('world')
// 調(diào)用實例的方法
console.log(greeter.greet())
如果你使用過 C# 或 Java,你會對這種語法非常熟悉。 我們聲明一個 Greeter 類。這個類有 3 個成員:一個叫做 message 的屬性,一個構(gòu)造函數(shù)和一個 greet 方法。
你會注意到,我們在引用任何一個類成員的時候都用了 this。 它表示我們訪問的是類的成員。
后面一行,我們使用 new 構(gòu)造了 Greeter 類的一個實例。它會調(diào)用之前定義的構(gòu)造函數(shù),創(chuàng)建一個 Greeter 類型的新對象,并執(zhí)行構(gòu)造函數(shù)初始化它。
最后一行通過 greeter 對象調(diào)用其 greet 方法
繼承
在 TypeScript 里,我們可以使用常用的面向?qū)ο竽J健?基于類的程序設(shè)計中一種最基本的模式是允許使用繼承來擴展現(xiàn)有的類。
看下面的例子:
/*
類的繼承
*/
class Animal {
run (distance: number) {
console.log(`Animal run ${distance}m`)
}
}
class Dog extends Animal {
cry () {
console.log('wang! wang!')
}
}
const dog = new Dog()
dog.cry()
dog.run(100) // 可以調(diào)用從父中繼承得到的方法
這個例子展示了最基本的繼承:類從基類中繼承了屬性和方法。 這里,Dog 是一個 派生類,它派生自 Animal 基類,通過 extends 關(guān)鍵字。 派生類通常被稱作子類,基類通常被稱作超類。
因為 Dog 繼承了 Animal 的功能,因此我們可以創(chuàng)建一個 Dog 的實例,它能夠 cry() 和 run()。
下面我們來看個更加復雜的例子:
class Animal {
name: string
constructor (name: string) {
this.name = name
}
run (distance: number=0) {
console.log(`${this.name} run ${distance}m`)
}
}
class Snake extends Animal {
constructor (name: string) {
// 調(diào)用父類型構(gòu)造方法
super(name)
}
// 重寫父類型的方法
run (distance: number=5) {
console.log('sliding...')
super.run(distance)
}
}
class Horse extends Animal {
constructor (name: string) {
// 調(diào)用父類型構(gòu)造方法
super(name)
}
// 重寫父類型的方法
run (distance: number=50) {
console.log('dashing...')
// 調(diào)用父類型的一般方法
super.run(distance)
}
xxx () {
console.log('xxx()')
}
}
const snake = new Snake('sn')
snake.run()
const horse = new Horse('ho')
horse.run()
// 父類型引用指向子類型的實例 ==> 多態(tài)
const tom: Animal = new Horse('ho22')
tom.run()
/* 如果子類型沒有擴展的方法, 可以讓子類型引用指向父類型的實例 */
const tom3: Snake = new Animal('tom3')
tom3.run()
/* 如果子類型有擴展的方法, 不能讓子類型引用指向父類型的實例 */
// const tom2: Horse = new Animal('tom2')
// tom2.run()
這個例子展示了一些上面沒有提到的特性。 這一次,我們使用 extends 關(guān)鍵字創(chuàng)建了 Animal 的兩個子類:Horse 和 Snake。
與前一個例子的不同點是,派生類包含了一個構(gòu)造函數(shù),它 必須調(diào)用 super(),它會執(zhí)行基類的構(gòu)造函數(shù)。 而且,在構(gòu)造函數(shù)里訪問 this 的屬性之前,我們 一定要調(diào)用 super()。 這個是 TypeScript 強制執(zhí)行的一條重要規(guī)則。
這個例子演示了如何在子類里可以重寫父類的方法。Snake類和 Horse 類都創(chuàng)建了 run 方法,它們重寫了從 Animal 繼承來的 run 方法,使得 run 方法根據(jù)不同的類而具有不同的功能。注意,即使 tom 被聲明為 Animal 類型,但因為它的值是 Horse,調(diào)用 tom.run(34) 時,它會調(diào)用 Horse 里重寫的方法。
sliding...
sn run 5m
dashing...
ho run 50m
修飾符
默認為 public 修飾符
在上面的例子里,我們可以自由的訪問程序里定義的成員。 如果你對其它語言中的類比較了解,就會注意到我們在之前的代碼里并沒有使用 public 來做修飾;例如,C# 要求必須明確地使用 public 指定成員是可見的。 在 TypeScript 里,成員都默認為 public。
你也可以明確的將一個成員標記成 public。 我們可以用下面的方式來重寫上面的 Animal 類:
private 修飾符
當成員被標記成 private 時,它就不能在聲明它的類的外部訪問。
protected 修飾符
protected 修飾符與 private 修飾符的行為很相似,但有一點不同,protected成員在派生類中仍然可以訪問。例如:
/*
訪問修飾符: 用來描述類內(nèi)部的屬性/方法的可訪問性
public: 默認值, 公開的外部也可以訪問
private: 只能類內(nèi)部可以訪問
protected: 類內(nèi)部和子類可以訪問
*/
class Animal {
public name: string
public constructor (name: string) {
this.name = name
}
public run (distance: number=0) {
console.log(`${this.name} run ${distance}m`)
}
}
class Person extends Animal {
private age: number = 18
protected sex: string = '男'
run (distance: number=5) {
console.log('Person jumping...')
super.run(distance)
}
}
class Student extends Person {
run (distance: number=6) {
console.log('Student jumping...')
console.log(this.sex) // 子類能看到父類中受保護的成員
// console.log(this.age) // 子類看不到父類中私有的成員
super.run(distance)
}
}
console.log(new Person('abc').name) // 公開的可見
// console.log(new Person('abc').sex) // 受保護的不可見
// console.log(new Person('abc').age) // 私有的不可見
readonly 修飾符
你可以使用 readonly 關(guān)鍵字將屬性設(shè)置為只讀的。 只讀屬性必須在聲明時或構(gòu)造函數(shù)里被初始化。
class Person {
readonly name: string = 'abc'
constructor(name: string) {
this.name = name
}
}
let john = new Person('John')
// john.name = 'peter' // error
參數(shù)屬性
在上面的例子中,我們必須在 Person 類里定義一個只讀成員 name 和一個參數(shù)為 name 的構(gòu)造函數(shù),并且立刻將 name 的值賦給 this.name,這種情況經(jīng)常會遇到。 參數(shù)屬性可以方便地讓我們在一個地方定義并初始化一個成員。 下面的例子是對之前 Person 類的修改版,使用了參數(shù)屬性:
class Person2 {
constructor(readonly name: string) {
}
}
const p = new Person2('jack')
console.log(p.name)
注意看我們是如何舍棄參數(shù) name,僅在構(gòu)造函數(shù)里使用 readonly name: string 參數(shù)來創(chuàng)建和初始化 name 成員。 我們把聲明和賦值合并至一處。
參數(shù)屬性通過給構(gòu)造函數(shù)參數(shù)前面添加一個訪問限定符來聲明。使用 private 限定一個參數(shù)屬性會聲明并初始化一個私有成員;對于 public 和 protected 來說也是一樣。
存取器
TypeScript 支持通過 getters/setters 來截取對對象成員的訪問。 它能幫助你有效的控制對對象成員的訪問。
下面來看如何把一個簡單的類改寫成使用 get 和 set。 首先,我們從一個沒有使用存取器的例子開始。
class Person {
firstName: string = 'A'
lastName: string = 'B'
get fullName () {
return this.firstName + '-' + this.lastName
}
set fullName (value) {
const names = value.split('-')
this.firstName = names[0]
this.lastName = names[1]
}
}
const p = new Person()
console.log(p.fullName)
p.firstName = 'C'
p.lastName = 'D'
console.log(p.fullName)
p.fullName = 'E-F'
console.log(p.firstName, p.lastName)
靜態(tài)屬性
到目前為止,我們只討論了類的實例成員,那些僅當類被實例化的時候才會被初始化的屬性。 我們也可以創(chuàng)建類的靜態(tài)成員,這些屬性存在于類本身上面而不是類的實例上。 在這個例子里,我們使用 static 定義 origin,因為它是所有網(wǎng)格都會用到的屬性。 每個實例想要訪問這個屬性的時候,都要在 origin 前面加上類名。 如同在實例屬性上使用 this.xxx 來訪問屬性一樣,這里我們使用 Grid.xxx 來訪問靜態(tài)屬性。
/*
靜態(tài)屬性, 是類對象的屬性
非靜態(tài)屬性, 是類的實例對象的屬性
*/
class Person {
name1: string = 'A'
static name2: string = 'B'
}
console.log(Person.name2)
console.log(new Person().name1)
抽象類
抽象類做為其它派生類的基類使用。 它們不能被實例化。不同于接口,抽象類可以包含成員的實現(xiàn)細節(jié)。 abstract 關(guān)鍵字是用于定義抽象類和在抽象類內(nèi)部定義抽象方法。
/*
抽象類
不能創(chuàng)建實例對象, 只有實現(xiàn)類才能創(chuàng)建實例
可以包含未實現(xiàn)的抽象方法
*/
abstract class Animal {
abstract cry ()
run () {
console.log('run()')
}
}
class Dog extends Animal {
cry () {
console.log(' Dog cry()')
}
}
const dog = new Dog()
dog.cry()
dog.run()