儘管走下去,沒必要逗留着採鮮花來保存,由於在這一路上,花天然會繼續開放。javascript
——泰戈爾《飛鳥集》java
在上篇文章裏,我對 TypeScript 中的類型聲明作了介紹,這塊也是 TypeScript 的基礎知識。講解的內容包括:typescript
: type
boolean
、number
、string
、null
、undefiend
和 symbol
interface Type {...}
type[]
或 (type1 | type2 | ...)[]
enum Colors {...}
接下來要介紹的包括:面向對象編程、訪問控制修飾符、類和接口、泛型。編程
咱們一個個來說。數組
ES6 引入了 class
關鍵字,爲 JavaScript 引入了相似於「類」的能力。bash
咱們先看一段 JavaScript 代碼:app
class Cat {
constructor(name) {
this.name = name
}
sayHi() {
return `Meow, my name is ${this.name}`
}
}
let tom = new Cat('Tom')
tom.sayHi() // Meow, my name is Tom
複製代碼
咱們聲明瞭一個類 Cat
,包含一個屬性 name
和方法 sayHi
。若是用 TypeScript 怎麼去改寫,添加類型限制呢?這樣作:編輯器
class Cat {
name: string;
sayHi(): string {
return `Meow, my name is ${this.name}`
}
}
let tom = new Cat()
tom.name = 'Tom'
複製代碼
上例咱們以給實例 tom
刻意添加屬性 name
的方式,來講明在類中聲明屬性類型的作法:在 TypeScript 類中,在使用諸如 this.name
的方式引入或設置實例屬性時,若是沒有提早以 prop: type
的方式聲明實例屬性的話,就會報錯:Property 'name' does not exist on type 'Cat'.
;同時,咱們限制實例方法的返回值類型是字符串。ide
JavaScript 並無私有屬性的概念,一般咱們使用約定或者函數做用域來實現「私有屬性」。TypeScript 就實現了這個特性,引入了訪問控制修飾符。函數
訪問控制修飾符就是一些關鍵字,用來限制類成員(屬性或方法)的訪問範圍。好比 public
用來修飾公用的屬性或方法。
類的修飾符包括:private
、protected
、和 public
(默認)。
對於類的每個屬性和方法,均可以在以前添加一個修飾符,限制其可訪問範圍:
class Cat {
name: string
constructor(name: string) {
this.name = name
}
sayHi(): string {
return `Meow, my name is ${this.name}`
}
}
複製代碼
這裏的 nam
是共有屬性,也就是說,經過 new Cat('foo')
返回一個實例對象後,咱們能夠經過 .name
的方式訪問 屬性 name
,可是若是咱們給 name
加了一個修飾符 private
,這裏的 name
就成爲私有屬性了,在實例上經過 .name
的方式再也不能訪問到。而關鍵字 protected
,則限制類成員只能在當前類(也就是本例中的 Cat
)及其子類中使用。
總結下來就是:
當前類 | 子類 | 實例 | |
---|---|---|---|
private |
✔️ | ||
protected |
✔️ | ✔️ | |
public |
✔️ | ✔️ | ✔️ |
繼承可以加強代碼的可複用性,將子類公用的方法和屬性抽象出來,一些表現不一致的子類,則能夠經過覆寫(override)的方式,將父類的屬性或者方法覆蓋掉,定義本身的邏輯。
// 在此定義一個 `Animal` 類
class Animal {
name: string;
constructor(name) {
this.name = name
}
// 包含一個方法 `sayHi`
sayHi() {
console.log('Hi');
}
}
// 類 `Cat` 繼承自 `Animal`
class Cat extends Animal {
constructor(name) {
super(name)
}
// 這裏定義的 `sayHi` 方法會覆蓋在父類中定義的
sayHi() {
console.log(`Meow, my name this ${this.name}`);
}
// 除了方法 `sayHi`,子類 `Cat` 還定義了本身的方法 `catchMouse`
catchMouse() {
console.log('捉到一隻老鼠')
}
}
複製代碼
接口是行爲的抽象。
舉個例子,看下面的代碼:
// 鳥類(`Bird`)包含兩個方法 `fly` 和 `jump`
class Bird {
fly() {
console.log('鳥在飛')
}
jump() {
console.log('鳥在跳')
}
}
// 蜜蜂(`Bee`)與鳥類相似有一個 `fly` 方法,初次以外,還擁有一個採蜜(`honey`)的能力。
class Bee {
fly() {
console.log('蜜蜂在飛')
}
honey() {
console.log('蜜蜂在採蜜')
}
}
複製代碼
Bird
和 Bee
中具備一個同名的方法,咱們能夠將類似類的共同的屬性和方法抽象出來,放在接口裏。
// 此處定義了一個接口 `Wings`
// 接口中定義了一個方法 `fly`,無返回值(void)
interface Wings {
fly(): void;
}
複製代碼
而後再讓 Bird
和 Bee
來實現這個接口:
// 使用關鍵字 `implements` 聲明要實現的接口
// 實現了某一接口的類(也就是這裏的 `Bird`),必須實現接口中定義的全部屬性或方法
class Bird implements Wings {
// 由於繼承了接口 `Wings`,所以必須實現 `fly` 方法
fly() {
console.log('鳥在飛')
}
// `jump` 是 `Bird` 中特有的方法
jump() {
console.log('鳥在跳')
}
}
// `Bee` 也實現了 `Wings`,所以也要實現 `fly` 方法
class Bee implements Wings {
fly() {
console.log('蜜蜂在飛')
}
// 除了 `fly`,蜜蜂也定義了本身的 `honey` 方法
honey() {
console.log('蜜蜂在採蜜')
}
}
複製代碼
因而可知,接口的做用:就是在一個統一的地方定義實現的接口,使實現接口的類有統一的行爲。
那麼繼承類和實現接口有什麼限制呢?答案是:一個類只能夠繼承自一個類,但能夠實現多個接口。
咱們舉個例子:
// 接口 `Wings`,定義了一個方法 `fly`
interface Wings {
fly(): void;
}
// 接口 `Mouth`,定義了一個方法 `sing`
interface Mouth {
sing(): void;
}
// 聲明一個抽象類 `Animal`
abstract class Animal {
// 用關鍵字 `abstract` 修飾的方法稱爲「抽象方法」
// 抽象方法是讓繼承類去實現的
abstract eat(): void;
}
// `Bird` 類繼承自 `Animal`,並實現了兩個接口 `Wings` 和 `Mouth`
class Bird extends Animal implements Wings, Mouth {
fly() {
console.log('鳥在飛')
}
eat() { ... } // 這個是接口 `Wings` 中定義的方法,必須實現
sing() { ... } // 這個是接口 `Mouth` 中定義的方法,必須實現
}
複製代碼
你們會會發現,抽象類與接口很是相似,那麼有何區別呢?首先說共同點:都定義了公共的方法,而後讓具體的類去實現。
而區別在於:
Bird
)就是動物(Animal
)的一種。注意:在抽象類中,除了能夠定義抽象方法,也能夠直接書寫實現的方法,這樣的話,繼承此抽象類的子類實例會自動具備這些已實現的方法了。
接口除了能夠繼承(extends
)接口外,還能夠繼承類。
// 聲明瞭一個類 `Dragon`
class Dragon {
fly() {
console.log('龍在飛')
}
}
// 接口 `FireDragon` 繼承了 `Dragon`
// 也就是說此接口額外多了一個 `'() => string'` 類型的方法 `fly` 的定義
interface FireDragon extends Dragon {
fire(): void
}
// 咱們將變量 `f` 的類型聲明爲 `FireGragon`,所以 `f` 必須實現 `fire` 和 `fly` 兩個方法
// 不然會報錯
let f: FireGragon = {
fire() {
console.log('龍在噴火')
}
fly() {
console.log('龍在飛')
}
}
複製代碼
若是須要設計一個函數:接收一個 number 類型參數,而後返回值也是 number 類型,該怎麼作呢?經過以前的學習,咱們能寫出:
function double(num: number): number {
return num * 2
}
複製代碼
那麼若是是這樣的:函數的返回值類型老是跟傳入的參數類型,保持一致,該怎麼作呢?這就引入了泛型的概念。
咱們在聲明函數時,能夠指定使用泛型。
咱們先寫一個函數:
// 咱們聲明瞭一個函數 `createArray`
function createArray(value, length) {
let arr = []
for (let i = 0; i < length; i++) {
arr[i] = value
}
return arr
}
let fooArray = createArray('foo', 3)
fooArray // ["foo", "foo", "foo"]
複製代碼
上面的 createArray
就是一個普通的函數,未使用 TypeScript 的類型限制。在 TypeScript 中,未聲明的變量缺省類型是 any
。所以上面代碼的寫法等同於:
function createArray(value: any, length: any): any[] {
let arr = []
for (let i = 0; i < length; i++) {
arr[i] = value
}
return arr
}
複製代碼
這樣致使的不方便的地方是,當咱們要操做數組裏的某一個成員時,因爲沒法得知成員類型,寫代碼是就不能給到便捷的代碼提示。
fooArray[0].??? // 因爲返回數組成員類型不肯定,編輯器不能很好地給咱們提供代碼提示
複製代碼
接下來咱們應用泛型:
function createArray<T>(value: T, length: number): T[] {
let arr = []
for (let i = 0; i < length; i++) {
arr[i] = value
}
return arr
}
let fooArray = createArray<string>('foo', 3)
fooArray[0].√ // 這裏,咱們就能獲得字符串相關的屬性/方法提示了
複製代碼
在函數名(即這裏的 createArray
)後面使用一對尖括號包裹字母的形式(func<T>
)聲明函數接收一個泛型 T
。其中,將參數和函數返回值類型也指定爲 T
了。如此一來,就能實現傳入的參數類型與函數返回值類型是一致的了。
接下來在上例中,調用函數時,指定當前泛型 T
所表明的類型是 string
,這樣函數的返回值——數組的類型也肯定了,也就是僅包含字符串成員的數組。
調用函數時,也能夠不用顯式經過
<xxx>
指明當前泛型所表示的具體類型。TypeScript 會自動根據傳入的參數 的類型,推斷出泛型T
所表明的類型。但筆者認爲這樣並不直觀,所以仍是建議在使用泛型時,顯式指明泛型類型。
咱們不難能看出,使用泛型的優點,就是咱們能夠動態調整 T
所表明的具體類型。好比,咱們稍微修成下面這樣:
let fooArray = createArray<number>(100, 3)
fooArray[0].√ // 此時,咱們就獲得數值相關的代碼提示了
複製代碼
注意,前面定義函數時使用的字符
T
只在函數調用以後,才能知道它所表示的具體類型;而且泛型做用域也僅侷限在聲明此泛型的函數做用域內。
前面函數代碼中的 let arr = []
因爲咱們沒有加類型,會被自動推測爲 any[]
,這裏也修改下:
function createArray<T>(value: T, length: number): T[] {
let arr: T[] = []
for (let i = 0; i < length; i++) {
arr[i] = value
}
return arr
}
複製代碼
咱們我麼不只在函數參數、返回值中使用了泛型 T
,還在函數做用域內使用了它。
請看下面的一個函數 swap
:
// 函數 `swap` 用於交換數組裏的兩個值
function swap(tuple) {
return [tuple[1], tuple[0]]
}
let swapped = swap(['foo', 3])
swapped // [3, "foo"]
複製代碼
函數 swap
的做用是顛倒一個數組裏兩個成員的順序。若是這兩個成員的類型是不一樣的,那麼咱們該如何去聲明這個數組呢?以下:
// 數組 `arr` 由兩個成員組成,第一個
let arr: [string, number] = ['hi', 8]
複製代碼
使用泛型改寫的話,就再也不是使用單個泛型了,而是須要使用兩個泛型,也就是多泛型。咱們須要這樣指定泛型:
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]]
}
複製代碼
不只是函數,接口也能夠應用泛型。
// 這裏咱們使用了一個接口 `ListApi`,使用了泛型 `T`
interface ListApi<T> {
data: T[]; // `data` 被聲明爲一個類型爲 `T` 的數組
// 除此以外,此接口還包含一個字符串屬性 `error_message` 和數值屬性 `status_code`
error_message: string;
status_code: number;
}
// 在此咱們將泛型 `T` 指定爲 `{ name: string; age: number }`
// `listResult` 成爲了一個肯定的類型了
let listResult: ListApi<{ name: string; age: number }>;
複製代碼
在定義類的時候,也可使用尖括號 <>
定義泛型。
// 這裏聲明瞭一個類 `Component`,指定使用了泛型 `T`
class Component<T> {
public props: T;
constructor(props: T) {
this.props = props;
}
}
// 定義了一個接口類型
interface ButtonProps {
color: string;
}
// 建立 `Component` 實例時,將 `T` 聲明爲 `ButtonProps` 類型
let button = new Component<ButtonProps>({
color: 'red'
});
複製代碼
這裏的 Component
表示一個組件類,須要傳入一個 props
,類型是泛型 T
所表明的具體類型。
在使用時,傳入了一個 ButtonProps
類型,那麼在初始化的時候,就要根據 ButtonProps
中定義的結構,傳入參數,是否是很靈活呢。
感謝你花費寶貴的時間閱讀這篇文章。
若是你以爲這篇文章讓你的生活美好了一點點,歡迎給我鮮(diǎn)花(zàn)或鼓(diǎn)勵(zàn)😀。若是能在文章下面留下你寶貴的評論或意見是再合適不過的了,由於研究證實,參與討論比單純閱讀更能讓人對知識印象深入,假期愉快😉~。
(完)