TypeScript學習(二) - TypeScript的接口(interface)和類(class)


1. 對象的類型——接口

在 TypeScript 中,我們使用接口(Interfaces)來定義對象的類型。

1.1 什么是接口

在面向對象語言中,接口(Interfaces)是一個很重要的概念,它是對行為的抽象,而具體如何行動需要由類(classes)去實現(implement)。

TypeScript 中的接口是一個非常靈活的概念,除了可用於對類的一部分行為進行抽象以外,也常用於對「對象的形狀(Shape)」進行描述。

1.2 簡單的例子

interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25
};

上面的例子中,我們定義了一個接口 Person,接着定義了一個變量 tom,它的類型是 Person。這樣,我們就約束了 tom 的形狀必須和接口 Person 一致。

接口一般首字母大寫。有的編程語言中會建議接口的名稱加上 I 前綴

定義的變量比接口少了一些屬性是不允許的:

interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom'
};

// index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
//   Property 'age' is missing in type '{ name: string; }'.

多一些屬性也是不允許的:

interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

// index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

可見,賦值的時候,變量的形狀必須和接口的形狀保持一致

1.3 可選屬性

有時我們希望不要完全匹配一個形狀,那么可以用可選屬性:

interface Person {
    name: string;
    age?: number;
}

let tom: Person = {
    name: 'Tom'
};
interface Person {
    name: string;
    age?: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25
};

可選屬性的含義是該屬性可以不存在。

這時仍然不允許添加未定義的屬性

interface Person {
    name: string;
    age?: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

// examples/playground/index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

1.4 任意屬性

有時候我們希望一個接口允許有任意的屬性,可以使用如下方式:

interface Person {
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};

使用 [propName: string] 定義了任意屬性取 string 類型的值。

需要注意的是,一旦定義了任意屬性,那么確定屬性和可選屬性的類型都必須是它的類型的子集

interface Person {
    name: string;
    age?: number;
    [propName: string]: string;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Index signatures are incompatible.
//     Type 'string | number' is not assignable to type 'string'.
//       Type 'number' is not assignable to type 'string'.

上例中,任意屬性的值允許是 string,但是可選屬性 age 的值卻是 numbernumber 不是 string 的子屬性,所以報錯了。

另外,在報錯信息中可以看出,此時 { name: 'Tom', age: 25, gender: 'male' } 的類型被推斷成了 { [x: string]: string | number; name: string; age: number; gender: string; },這是聯合類型和接口的結合。

一個接口中只能定義一個任意屬性。如果接口中有多個類型的屬性,則可以在任意屬性中使用聯合類型:

interface Person {
    name: string;
    age?: number;
    [propName: string]: string | number;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

1.5 只讀屬性

有時候我們希望對象中的一些字段只能在創建的時候被賦值,那么可以用 readonly 定義只讀屬性:

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    id: 89757,
    name: 'Tom',
    gender: 'male'
};

tom.id = 9527;

// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

上例中,使用 readonly 定義的屬性 id 初始化后,又被賦值了,所以報錯了。

注意,只讀的約束存在於第一次給對象賦值的時候,而不是第一次給只讀屬性賦值的時候

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};

tom.id = 89757;

// index.ts(8,5): error TS2322: Type '{ name: string; gender: string; }' is not assignable to type 'Person'.
//   Property 'id' is missing in type '{ name: string; gender: string; }'.
// index.ts(13,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

上例中,報錯信息有兩處,第一處是在對 tom 進行賦值的時候,沒有給 id 賦值。

第二處是在給 tom.id 賦值的時候,由於它是只讀屬性,所以報錯了。

2. 類

傳統方法中,JavaScript 通過構造函數實現類的概念,通過原型鏈實現繼承。而在 ES6 中,我們終於迎來了 class

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

這一節主要介紹類的用法,下一節再介紹如何定義類的類型。

2.1 類的概念

雖然 JavaScript 中有類的概念,但是可能大多數 JavaScript 程序員並不是非常熟悉類,這里對類相關的概念做一個簡單的介紹。

  • 類(Class):定義了一件事物的抽象特點,包含它的屬性和方法
  • 對象(Object):類的實例,通過 new 生成
  • 面向對象(OOP)的三大特性:封裝、繼承、多態
  • 封裝(Encapsulation):將對數據的操作細節隱藏起來,只暴露對外的接口。外界調用端不需要(也不可能)知道細節,就能通過對外提供的接口來訪問該對象,同時也保證了外界無法任意更改對象內部的數據
  • 繼承(Inheritance):子類繼承父類,子類除了擁有父類的所有特性外,還有一些更具體的特性
  • 多態(Polymorphism):由繼承而產生了相關的不同的類,對同一個方法可以有不同的響應。比如 CatDog 都繼承自 Animal,但是分別實現了自己的 eat 方法。此時針對某一個實例,我們無需了解它是 Cat 還是 Dog,就可以直接調用 eat 方法,程序會自動判斷出來應該如何執行 eat
  • 存取器(getter & setter):用以改變屬性的讀取和賦值行為
  • 修飾符(Modifiers):修飾符是一些關鍵字,用於限定成員或類型的性質。比如 public 表示公有屬性或方法
  • 抽象類(Abstract Class):抽象類是供其他類繼承的基類,抽象類不允許被實例化。抽象類中的抽象方法必須在子類中被實現
  • 接口(Interfaces):不同類之間公有的屬性或方法,可以抽象成一個接口。接口可以被類實現(implements)。一個類只能繼承自另一個類,但是可以實現多個接口

2.2 ES6 中類的用法

下面我們先回顧一下 ES6 中類的用法,更詳細的介紹可以參考 [ECMAScript 6 入門 - Class]。

屬性和方法

使用 class 定義類,使用 constructor 定義構造函數。

通過 new 生成新實例的時候,會自動調用構造函數。

class Animal {
    public name;
    constructor(name) {
        this.name = name;
    }
    sayHi() {
        return `My name is ${this.name}`;
    }
}

let a = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack

類的繼承

使用 extends 關鍵字實現繼承,子類中使用 super 關鍵字來調用父類的構造函數和方法。

class Cat extends Animal {
  constructor(name) {
    super(name); // 調用父類的 constructor(name)
    console.log(this.name);
  }
  sayHi() {
    return 'Meow, ' + super.sayHi(); // 調用父類的 sayHi()
  }
}

let c = new Cat('Tom'); // Tom
console.log(c.sayHi()); // Meow, My name is Tom

存取器

使用 getter 和 setter 可以改變屬性的賦值和讀取行為:

class Animal {
  constructor(name) {
    this.name = name;
  }
  get name() {
    return 'Jack';
  }
  set name(value) {
    console.log('setter: ' + value);
  }
}

let a = new Animal('Kitty'); // setter: Kitty
a.name = 'Tom'; // setter: Tom
console.log(a.name); // Jack

靜態方法

使用 static 修飾符修飾的方法稱為靜態方法,它們不需要實例化,而是直接通過類來調用:

class Animal {
  static isAnimal(a) {
    return a instanceof Animal;
  }
}

let a = new Animal('Jack');
Animal.isAnimal(a); // true
a.isAnimal(a); // TypeError: a.isAnimal is not a function

2.3 ES7 中類的用法

ES7 中有一些關於類的提案,TypeScript 也實現了它們,這里做一個簡單的介紹。

實例屬性

ES6 中實例的屬性只能通過構造函數中的 this.xxx 來定義,ES7 提案中可以直接在類里面定義:

class Animal {
  name = 'Jack';

  constructor() {
    // ...
  }
}

let a = new Animal();
console.log(a.name); // Jack

靜態屬性

ES7 提案中,可以使用 static 定義一個靜態屬性:

class Animal {
  static num = 42;

  constructor() {
    // ...
  }
}

console.log(Animal.num); // 42

2.4 TypeScript 中類的用法

public private 和 protected

TypeScript 可以使用三種訪問修飾符(Access Modifiers),分別是 publicprivateprotected

  • public 修飾的屬性或方法是公有的,可以在任何地方被訪問到,默認所有的屬性和方法都是 public
  • private 修飾的屬性或方法是私有的,不能在聲明它的類的外部訪問
  • protected 修飾的屬性或方法是受保護的,它和 private 類似,區別是它在子類中也是允許被訪問的

下面舉一些例子:

class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';
console.log(a.name); // Tom

上面的例子中,name 被設置為了 public,所以直接訪問實例的 name 屬性是允許的。

很多時候,我們希望有的屬性是無法直接存取的,這時候就可以用 private 了:

class Animal {
  private name;
  public constructor(name) {
    this.name = name;
  }
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';

// index.ts(9,13): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
// index.ts(10,1): error TS2341: Property 'name' is private and only accessible within class 'Animal'.

需要注意的是,TypeScript 編譯之后的代碼中,並沒有限制 private 屬性在外部的可訪問性。

上面的例子編譯后的代碼是:

var Animal = (function () {
  function Animal(name) {
    this.name = name;
  }
  return Animal;
})();
var a = new Animal('Jack');
console.log(a.name);
a.name = 'Tom';

使用 private 修飾的屬性或方法,在子類中也是不允許訪問的:

class Animal {
  private name;
  public constructor(name) {
    this.name = name;
  }
}

class Cat extends Animal {
  constructor(name) {
    super(name);
    console.log(this.name);
  }
}

// index.ts(11,17): error TS2341: Property 'name' is private and only accessible within class 'Animal'.

而如果是用 protected 修飾,則允許在子類中訪問:

class Animal {
  protected name;
  public constructor(name) {
    this.name = name;
  }
}

class Cat extends Animal {
  constructor(name) {
    super(name);
    console.log(this.name);
  }
}

當構造函數修飾為 private 時,該類不允許被繼承或者實例化:

class Animal {
  public name;
  private constructor(name) {
    this.name = name;
  }
}
class Cat extends Animal {
  constructor(name) {
    super(name);
  }
}

let a = new Animal('Jack');

// index.ts(7,19): TS2675: Cannot extend a class 'Animal'. Class constructor is marked as private.
// index.ts(13,9): TS2673: Constructor of class 'Animal' is private and only accessible within the class declaration.

當構造函數修飾為 protected 時,該類只允許被繼承:

class Animal {
  public name;
  protected constructor(name) {
    this.name = name;
  }
}
class Cat extends Animal {
  constructor(name) {
    super(name);
  }
}

let a = new Animal('Jack');

// index.ts(13,9): TS2674: Constructor of class 'Animal' is protected and only accessible within the class declaration.

參數屬性

修飾符和readonly還可以使用在構造函數參數中,等同於類中定義該屬性同時給該屬性賦值,使代碼更簡潔。

class Animal {
  // public name: string;
  public constructor(public name) {
    // this.name = name;
  }
}

readonly

只讀屬性關鍵字,只允許出現在屬性聲明或索引簽名或構造函數中。

class Animal {
  readonly name;
  public constructor(name) {
    this.name = name;
  }
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';

// index.ts(10,3): TS2540: Cannot assign to 'name' because it is a read-only property.

注意如果 readonly 和其他訪問修飾符同時存在的話,需要寫在其后面。

class Animal {
  // public readonly name;
  public constructor(public readonly name) {
    // this.name = name;
  }
}

抽象類

abstract 用於定義抽象類和其中的抽象方法。

什么是抽象類?

首先,抽象類是不允許被實例化的:

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

let a = new Animal('Jack');

// index.ts(9,11): error TS2511: Cannot create an instance of the abstract class 'Animal'.

上面的例子中,我們定義了一個抽象類 Animal,並且定義了一個抽象方法 sayHi。在實例化抽象類的時候報錯了。

其次,抽象類中的抽象方法必須被子類實現:

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

class Cat extends Animal {
  public eat() {
    console.log(`${this.name} is eating.`);
  }
}

let cat = new Cat('Tom');

// index.ts(9,7): error TS2515: Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.

上面的例子中,我們定義了一個類 Cat 繼承了抽象類 Animal,但是沒有實現抽象方法 sayHi,所以編譯報錯了。

下面是一個正確使用抽象類的例子:

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

class Cat extends Animal {
  public sayHi() {
    console.log(`Meow, My name is ${this.name}`);
  }
}

let cat = new Cat('Tom');

上面的例子中,我們實現了抽象方法 sayHi,編譯通過了。

需要注意的是,即使是抽象方法,TypeScript 的編譯結果中,仍然會存在這個類,上面的代碼的編譯結果是:

var __extends =
  (this && this.__extends) ||
  function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() {
      this.constructor = d;
    }
    d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new __());
  };
var Animal = (function () {
  function Animal(name) {
    this.name = name;
  }
  return Animal;
})();
var Cat = (function (_super) {
  __extends(Cat, _super);
  function Cat() {
    _super.apply(this, arguments);
  }
  Cat.prototype.sayHi = function () {
    console.log('Meow, My name is ' + this.name);
  };
  return Cat;
})(Animal);
var cat = new Cat('Tom');

2.5 類的類型

給類加上 TypeScript 的類型很簡單,與接口類似:

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  sayHi(): string {
    return `My name is ${this.name}`;
  }
}

let a: Animal = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack

3. 類與接口

之前學習過,接口(Interfaces)可以用於對「對象的形狀(Shape)」進行描述。

這一章主要介紹接口的另一個用途,對類的一部分行為進行抽象。

3.1 類實現接口

實現(implements)是面向對象中的一個重要概念。一般來講,一個類只能繼承自另一個類,有時候不同類之間可以有一些共有的特性,這時候就可以把特性提取成接口(interfaces),用 implements 關鍵字來實現。這個特性大大提高了面向對象的靈活性。

舉例來說,門是一個類,防盜門是門的子類。如果防盜門有一個報警器的功能,我們可以簡單的給防盜門添加一個報警方法。這時候如果有另一個類,車,也有報警器的功能,就可以考慮把報警器提取出來,作為一個接口,防盜門和車都去實現它:

interface Alarm {
    alert(): void;
}

class Door {
}

class SecurityDoor extends Door implements Alarm {
    alert() {
        console.log('SecurityDoor alert');
    }
}

class Car implements Alarm {
    alert() {
        console.log('Car alert');
    }
}

一個類可以實現多個接口:

interface Alarm {
    alert(): void;
}

interface Light {
    lightOn(): void;
    lightOff(): void;
}

class Car implements Alarm, Light {
    alert() {
        console.log('Car alert');
    }
    lightOn() {
        console.log('Car light on');
    }
    lightOff() {
        console.log('Car light off');
    }
}

上例中,Car 實現了 AlarmLight 接口,既能報警,也能開關車燈。

3.2 接口繼承接口

接口與接口之間可以是繼承關系:

interface Alarm {
    alert(): void;
}

interface LightableAlarm extends Alarm {
    lightOn(): void;
    lightOff(): void;
}

這很好理解,LightableAlarm 繼承了 Alarm,除了擁有 alert 方法之外,還擁有兩個新方法 lightOnlightOff

3.3 接口繼承類

常見的面向對象語言中,接口是不能繼承類的,但是在 TypeScript 中卻是可以的:

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

為什么 TypeScript 會支持接口繼承類呢?

實際上,當我們在聲明 class Point 時,除了會創建一個名為 Point 的類之外,同時也創建了一個名為 Point 的類型(實例的類型)。

所以我們既可以將 Point 當做一個類來用(使用 new Point 創建它的實例):

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

const p = new Point(1, 2);

也可以將 Point 當做一個類型來用(使用 : Point 表示參數的類型):

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

function printPoint(p: Point) {
    console.log(p.x, p.y);
}

printPoint(new Point(1, 2));

這個例子實際上可以等價於:

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

interface PointInstanceType {
    x: number;
    y: number;
}

function printPoint(p: PointInstanceType) {
    console.log(p.x, p.y);
}

printPoint(new Point(1, 2));

上例中我們新聲明的 PointInstanceType 類型,與聲明 class Point 時創建的 Point 類型是等價的。

所以回到 Point3d 的例子中,我們就能很容易的理解為什么 TypeScript 會支持接口繼承類了:

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

interface PointInstanceType {
    x: number;
    y: number;
}

// 等價於 interface Point3d extends PointInstanceType
interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

當我們聲明 interface Point3d extends Point 時,Point3d 繼承的實際上是類 Point 的實例的類型。

換句話說,可以理解為定義了一個接口 Point3d 繼承另一個接口 PointInstanceType

所以「接口繼承類」和「接口繼承接口」沒有什么本質的區別。

值得注意的是,PointInstanceType 相比於 Point,缺少了 constructor 方法,這是因為聲明 Point 類時創建的 Point 類型是不包含構造函數的。另外,除了構造函數是不包含的,靜態屬性或靜態方法也是不包含的(實例的類型當然不應該包括構造函數、靜態屬性或靜態方法)。

換句話說,聲明 Point 類時創建的 Point 類型只包含其中的實例屬性和實例方法:

class Point {
    /** 靜態屬性,坐標系原點 */
    static origin = new Point(0, 0);
    /** 靜態方法,計算與原點距離 */
    static distanceToOrigin(p: Point) {
        return Math.sqrt(p.x * p.x + p.y * p.y);
    }
    /** 實例屬性,x 軸的值 */
    x: number;
    /** 實例屬性,y 軸的值 */
    y: number;
    /** 構造函數 */
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
    /** 實例方法,打印此點 */
    printPoint() {
        console.log(this.x, this.y);
    }
}

interface PointInstanceType {
    x: number;
    y: number;
    printPoint(): void;
}

let p1: Point;
let p2: PointInstanceType;

上例中最后的類型 Point 和類型 PointInstanceType 是等價的。

同樣的,在接口繼承類的時候,也只會繼承它的實例屬性和實例方法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM