TypeScript 簡明教程:介面、函式與類

Hopsken發表於2019-03-31

本文為系列文章《TypeScript 簡明教程》中的一篇。

介面

TypeScript 中,我們使用介面來描述物件或類的具體結構。介面的概念在 TypeScript 中是至關重要的。它就像是你與程式簽訂的一個契約,定義一個介面就意味著你答應程式:未來的某個值(或者類)一定會符合契約中所規定的模樣,如果不符合,TS 就會直接在編譯時報錯

感興趣的同學可以瞭解一下 鴨子型別

舉個例子:

interface Phone {
    model: string
    price: number
}

let newPhone: Phone = {
    model: 'iPhone XS',
    price: 8599,
}
複製程式碼

上面的例子中,我們定義了一個介面 Phone,它約定:任何型別為 Phone 的值,有且只能有兩個屬性:string 型別的 model 屬性以及 number 型別的 price 屬性。之後,我們宣告瞭一個變數 newPhone,它的型別為 Phone,而且遵照契約,將 model 賦值為字串,price 賦值為數值。

介面一般首字母大寫。在某些程式語言會建議使用 I 作為字首。關於是否要使用 I 字首,tslint 有一條 專門的規則,請根據團隊編碼風格自行選擇。

多一些屬性和少一些屬性都是不允許的。

let phoneA: Phone = {
    model: 'iPhone XS',
} // Error: Property 'price' is missing in type '{ model: string; }' but required in type 'Phone'

let phoneB: Phone = {
    model: 'iPhone XS',
    price: 8599,
    producer: 'Apple',
} // Error: Property 'producer' doesn't exist on type `Phone`.
複製程式碼

介面作為型別註解,只在編譯時起作用,不會出現在最終輸出的 JS 程式碼中。

可選屬性

對於某個可能存在的屬性,我們可以在該屬性後加上 ?標記 表示這個屬性是可選的。

interface Phone {
    model: string
    price: number
    producer?: string
}

let newPhone: Phone = {
    model: 'iPhone XS',
    price: 8599, // OK
}

let phoneB: Phone = {
    model: 'iPhone XS',
    price: 8599,
    producer: 'Apple', // OK
} 
複製程式碼

任意屬性

某些情況下,我們可能只知道介面中部分屬性及它們的型別。或者,我們希望能夠在初始化後動態新增物件的屬性。這時,我們可以使用下面這種寫法。

interface Phone {
    model: string
    price: number
    producer?: string
    [propName: string]: any
}

let phoneB: Phone = {
    model: 'iPhone XS',
    price: 8599,
} 
phoneB.storage = '256GB' // OK
複製程式碼

上面,我們定義任意屬性的簽名為 string 型別,值為 any 型別。注意:任意屬性的值型別必須包含所有已知屬性的值型別。 上述例子中,any 包括 stringnumber 型別。

只讀屬性

介面中,我們可以使用 readonly 標記某個屬性是隻讀的。當我們試圖修改它時,TS 會提示報錯。

interface Phone {
    readonly model: string
    price: number
}

let phoneA: Phone = {
    model: 'iPhone XS',
    price: 8599,
}
phoneA.model = 'iPhone Air' // Error: Cannot assign to 'model' because it is a read-only property.
複製程式碼

函式

呼,終於說到函式了。JavaScript 中有兩種定義函式的方法。

// 命名函式
function add(x, y) {
    return x + y
}

// 匿名函式
const add = function(x, y) { return x + y }
複製程式碼

對於這兩種方法,新增型別註釋的方式大同小異。

// 命名函式
function add(x: number, y: number): number {
    return x + y
}

// 匿名函式
const add = function(x: number, y: number): number { 
    return x + y
}
複製程式碼

上面我們定義了 add 函式,它接受兩個 number 型別的引數,並規定其返回值為 number 型別。

呼叫函式時,傳入引數的型別和數量必須與定義時保持一致。

add(1, 2) // OK
add('1', 0) // Error
add(1, 2, 3) // Error
複製程式碼

可選引數

使用 ?標記 可以標識某個引數是可選的。可選引數必須放在必要引數後面。

function increment(x: number, step?: number): number {
    return x + (step || 1)
}

increment(10) // => 11
複製程式碼

引數預設值

ES6 允許我們為引數新增預設值。作為 JS 的超集,TS 自然也是支援引數預設值的。

function increment(x: number, step: number = 1): number {
    return x + step
}

increment(10) // => 11
複製程式碼

因為具有引數預設值的引數必然是可選引數,所以無需再使用 ? 標記該引數時可選的。

這裡,step: number = 1 可以簡寫為 step = 1,TS 會根據型別推斷自動推斷出 step 應為 number 型別。

與可選引數不同的是,具有預設值的引數不必放在必要引數後面。下面的寫法也是允許的,只是在呼叫時,必須明確地傳入 undefined 來獲取預設值。

function increment(step = 1, x: number): number {
    return x + step
}

increment(undefined, 10) // => 11
複製程式碼

剩餘引數

ES6 允許我們使用剩餘引數將一個不定數量的參數列示為一個陣列。TypeScript 中我們可以這樣寫。

function sum(...args: number[]): number {
    return args.reduce((prev, cur) => prev + cur)
}

sum(1, 2, 3) // => 6
複製程式碼

注意與 arguments 物件進行 區分

介面中的方法

對於介面中的方法,我們可以使用如下方式去定義:

interface Animal {
    say(text: string): void
}

// 或者 
interface Animal {
    say: (text: string) => void
}
複製程式碼

這兩種註解方法的效果是一致的。

函式過載

函式過載允許你針對不同的引數進行不同的處理,進而返回不同的資料。

因為 JavaScript 在語言層面並不支援過載,我們必須在函式體內自行判斷引數進行鍼對性處理,從而模擬出函式過載。

function margin(all: number);
function margin(vertical: number, horizontal: number);
function margin(top: number, right: number, bottom: number, left: number);
function margin(a: number, b?: number, c?: number, d?: number) {
    if (b === undefined && c === undefined && d === undefined) {
        b = c = d = a
    } else if (c === undefined && d === undefined) {
        c = a
        d = b
    }

    return {
        top: a,
        right: b,
        bottom: c,
        left: d,
    }
}

console.log(margin(10))
// => { top: 10, right: 10, bottom: 10, left: 10 } 
console.log(margin(10, 20))
// => { top: 10, right: 20, bottom: 10, left: 20 }
console.log(margin(10, 20, 20, 20))
// => { top: 10, right: 20, bottom: 20, left: 20 }
console.log(margin(10, 20, 20))
// Error
複製程式碼

上述例子中,前面三個宣告瞭三種函式定義,編譯器會根據這個順序來處理函式呼叫,最後一個為最終的函式實現。需要注意的是,最後的函式實現引數型別必須包含之前所有的引數型別定義。因此,在定義過載的時候,一定要把最精確的定義放在最前面。

以前,JavaScript 中並沒有類的概念,我們使用原型來模擬類的繼承,直到 ES6 的出現,引入了 class 關鍵字。如果你對 ES6 的 class 還不是很瞭解,建議閱讀 ECMAScript 6 入門 - Class

TypeScript 除了實現了所有 ES6 中的類的功能以外,還新增了一些新的用法。

訪問修飾符

TypeScript 中可以使用三種修飾符:publicprivateprotected

public 修飾符

表示屬性或方法是公有的,在類內部、子類內部、類的例項中都能被訪問。預設情況下,所有屬性和方法都是 public 的。

class Animal {
    public name: string
    
    constructor(name) {
        this.name = name
    }
}
    
let cat = new Animal('Tom')
console.log(cat.name); // => Tom
複製程式碼

private 修飾符

表示屬性或方法是私有的,只能在類內部訪問。

class Animal {
    private name: string
    
    constructor(name) {
        this.name = name
    }
    
    greet() {
        return `Hello, my name is ${ this.name }.`
    }
}
    
let cat = new Animal('Tom')
console.log(cat.name); // Error: 屬性“name”為私有屬性,只能在類“Animal”中訪問。
console.log(cat.greet()) // => Hello, my name is Tom. 
複製程式碼

protected 修飾符

表示屬性或方法是受保護的,與 private 近似,不過被 protected 修飾的屬性或方法也能被其子類訪問。

class Animal {
    protected name: string
    
    constructor(name) {
        this.name = name
    }
}
    
class Cat extends Animal {
    constructor(name) {
        super(name)
    }
    
    greet() {
        return `Hello, I'm ${ this.name } the cat.`
    }
}
    
let cat = new Cat('Tom')
console.log(cat.name); // Error: 屬性“name”受保護,只能在類“Animal”及其子類中訪問。
console.log(cat.greet()) // => Hello, I'm Tom the cat. 
複製程式碼

注意,TypeScript 只做編譯時檢查,當你試圖在類外部訪問被 private 或者 protected 修飾的屬性或方法時,TS 會報錯,但是它並不能阻止你訪問這些屬性或方法。

目前有一個提案,建議在語言層面使用 # 字首標記某個屬性或方法為私有的,感興趣的可以看 這裡

抽象類

抽象類是某個類具體實現的抽象表述,作為其他類的基類使用。

它具有兩個特點:

  1. 不能被例項化
  2. 其抽象方法必須被子類實現

TypeScript 中使用 abstract 關鍵字表示抽象類以及其內部的抽象方法。

繼續使用上面的 Animal 類的例子:

abstract class Animal {
    public abstract makeSound(): void
    public move() {
        console.log('Roaming...')
    }
}

class Cat extends Animal {
    makeSound() {
        console.log('Meow~')
    }
}

let tom = new Cat()
tom.makeSound() // => 'Meow~'
tom.move() // => 'Roaming...'
複製程式碼

上述例子中,我們建立了一個抽象類 Animal,它定義了一個抽象方法 makeSound。然後,我們定義了一個 Cat 類,繼承自 Animal。因為 Animal 定義了 makeSound 抽象類,所以我們必須在 Cat 類裡面實現它。不然的話,TS 會報錯。

// Error: 非抽象類“Cat”沒有實現繼承自“Animal”類的抽象成員“makeSound”。
class Cat extends Animal {
    meow() {
       console.log('Meow~')  
    }
}
複製程式碼

類與介面

類可以實現(implement)介面。通過介面,你可以強制地指明類遵守某個契約。你可以在介面中宣告一個方法,然後要求類去具體實現它。

interface ClockInterface {
    currentTime: Date
    setTime(d: Date)
}

class Clock implements ClockInterface {
    currentTime: Date
    setTime(d: Date) {
        this.currentTime = d
    }
}
複製程式碼

介面與抽象類的區別

  1. 類可以實現(implement)多個介面,但只能擴充套件(extends)自一個抽象類。
  2. 抽象類中可以包含具體實現,介面不能。
  3. 抽象類在執行時是可見的,可以通過 instanceof 判斷。介面則只在編譯時起作用。
  4. 介面只能描述類的公共(public)部分,不會檢查私有成員,而抽象類沒有這樣的限制。

小結

本篇主要介紹了 TypeScript 中的幾個重要概念:介面、函式和類,知道了如何用介面去描述物件的結構,如何去描述函式的型別以及 TypeScript 中類的用法。

相關文章