TypeScript 從據說到入門(下篇)

儘管走下去,沒必要逗留着採鮮花來保存,由於在這一路上,花天然會繼續開放。javascript

——泰戈爾《飛鳥集》java

上篇文章裏,我對 TypeScript 中的類型聲明作了介紹,這塊也是 TypeScript 的基礎知識。講解的內容包括:typescript

  1. 指定類型的語法:: type
  2. 指定爲基本類型:booleannumberstringnullundefiendsymbol
  3. 指定爲對象類型
    • 普通對象:interface Type {...}
    • 數組:type[](type1 | type2 | ...)[]
  4. 擴展類型
    • 字面量類型:字符串字面量、數值字面量和布爾值字面量
    • 枚舉: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 用來修飾公用的屬性或方法。

類的修飾符包括:privateprotected、和 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('蜜蜂在採蜜')
    }
}
複製代碼

BirdBee 中具備一個同名的方法,咱們能夠將類似類的共同的屬性和方法抽象出來,放在接口裏。

// 此處定義了一個接口 `Wings`
// 接口中定義了一個方法 `fly`,無返回值(void)
interface Wings {
    fly(): void;
}
複製代碼

而後再讓 BirdBee 來實現這個接口:

// 使用關鍵字 `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` 中定義的方法,必須實現
}
複製代碼

你們會會發現,抽象類與接口很是相似,那麼有何區別呢?首先說共同點:都定義了公共的方法,而後讓具體的類去實現。

而區別在於:

  1. 接口就像插件同樣,是用來加強類的,而抽象類則是具體類的抽象概念。好比這裏的鳥(Bird)就是動物(Animal)的一種。
  2. 類實現接口是多對多的關係,一個類能夠實現多個接口,一個接口也能夠被多類實現;而類繼承類則是一對多的關係,一個類的父類只能有一個,一個類的子類則能夠有多個。

注意:在抽象類中,除了能夠定義抽象方法,也能夠直接書寫實現的方法,這樣的話,繼承此抽象類的子類實例會自動具備這些已實現的方法了。

接口繼承類

接口除了能夠繼承(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)😀。若是能在文章下面留下你寶貴的評論或意見是再合適不過的了,由於研究證實,參與討論比單純閱讀更能讓人對知識印象深入,假期愉快😉~。

(完)

相關文章
相關標籤/搜索