跳到主要內容
黯羽輕揚每天積累一點點

類_TypeScript 筆記 4

免費2019-01-27#TypeScript#TypeScript Class#TS类继承#TS访问控制#TypeScript成员修饰符

不僅成員修飾符、抽象類等特性全開,還不會增加運行時開銷

一.類成員

TypeScript 裡的類的定義與 [ES6 Class 規範](/articles/class-es6 筆記 10/) 一致,靜態屬性,實例屬性,訪問器等都支持:

class Grid {
    static origin = {x: 0, y: 0};
}

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        this._fullName = newName;
    }
}

但需要注意 2 點:

  • ES3 不支持 getter/setter,因此要求編譯配置為 ES5+

  • 只有 getter 沒有 setter 的屬性會被自動推斷為 readonly(成員修飾符之一)

二.成員修飾符

訪問控制修飾符

支持 3 個訪問控制修飾符:

  • public:類的成員屬性/方法默認都是 public,沒有訪問限制

  • private:無法在該類聲明的外部訪問其成員(如無法通過 this.xxx 訪問私有成員)

  • protected:與 private 類似,但在派生類中也可以訪問受保護成員

例如:

class Animal {
    // 私有成員屬性
    private name: string;
    // 受保護成員屬性
    protected ancestor: string;
    // 不寫的話,默認 public
    constructor(theName: string) { this.name = theName; }
    // public 成員方法
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

注意,這些訪問控制都只是編譯時的限制,運行時並不強檢查。符合 TypeScript 的設計原則:

不給編譯產物增加運行時開���

另外,類成員可訪問性也是類型檢查的一部分,private/protected 修飾符會打破鴨子類型,例如:

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
}
class Person {
    name: string;
    constructor(theName: string) { this.name = theName; }
}
// 正確:鴨子類型(長得像就是)
let person: Animal = new Person('Sam');

class Plant {
    // 私有成員 name
    private name: string;
    constructor(theName: string) { this.name = theName; }
}
// 錯誤:name 屬性可訪問性不匹配(長得像也不行,可訪問性不一樣)
// Property 'name' is private in type 'Plant' but not in type 'Person'.
let invalidPlant: Plant = new Person('Stone');

P.S.特殊的,protected constructor 表示該類不允許直接實例化,但支持繼承

readonly 修飾符

可以通過 readonly 修飾符聲明屬性只讀,只能在聲明時或構造函數裡賦值,例如:

class Octopus {
    readonly name: string;
    public readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // error! name is readonly.

P.S.當然,readonly 與訪問控制修飾符並不衝突,可以作用於同一個屬性

參數屬性

對於在構造函數裡初始化的屬性:

class Octopus {
    readonly name: string;
    constructor (theName: string) {
        this.name = theName;
    }
}

有一種簡寫方式

class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {
    }
}

其中,name參數屬性,通過給構造函數的形參名前添上 private/protected/public/readonly 修飾符來聲明

三.繼承

class A extends B {
    //...
}

類似於 Babel 轉換(清晰起見,僅摘錄關鍵部分):

function _inherits(subClass, superClass) {
  // 繼承父類原型屬性
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
  // 繼承父類靜態屬性
  if (superClass)
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subClass, superClass)
      : (subClass.__proto__ = superClass);
}

// 子類構造函數中繼承父類實例屬性
function A() {
    // 通過父類構造函數給子類實例 this 添上父類實例屬性
    return A.__proto__ || Object.getPrototypeOf(A)).apply(this, arguments)
}

TypeScript 裡的 Class 繼承也會被編譯替換成基於原型的繼承,如下:

var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };

    return function (d, b) {
        // 繼承父類靜態屬性
        extendStatics(d, b);
        function __() { this.constructor = d; }
        // 繼承父類原型屬性,及實例屬性
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();

// __extends(A, B);

二者大同小異,從實現上看,TypeScript 編譯產物更健壯,因為其目標是:

在任何支持 ES3+ 的宿主環境中運行

P.S.比較有意思的是靜態屬性的繼承,具體見 [一。如何繼承靜態屬性?](/articles/class 繼承-es6 筆記 12/#articleHeader2)

四.抽象類

TypeScript 裡也有抽象類的概念:

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}

抽象類裡可以有帶實現的具體方法(如 move),也可以有只聲明不實現的抽象方法(如 makeSound),但要求子類必須實現這些方法:

class Cat extends Animal {
    makeSound() {
        console.log('meow meow meow');
    }
}

另一個相似的概念是接口,二者區別在於接口中只能定義「抽象方法」(沒有 abstract 關鍵字,確切地說是方法簽名),例如:

interface Animal {
  // 對比 abstract makeSound(): void;
  makeSound(): void;
}

// 對比 class Cat extends Animal
class Cat implements Animal {
    makeSound() {
        console.log('meow meow meow');
    }
}

五.類與類型

聲明一個類的同時,也聲明瞭該類實例的類型,例如:

class Greeter {
    static standardGreeting = "Hello, there";
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter: Greeter;
greeter = new Greeter("world");

其中,實例 greeterGreetr 類型的,也就是說,Class 聲明具有類型含義

  • 該類實例的類型:Greeter

  • 類自身的類型:typeof Greeter

實際上,類自身的類型約束了靜態屬性、實例屬性、構造函數、原型方法等特徵,例如:

class GreeterDuck {
    // 類自身的類型約束
    static standardGreeting = 'Hi';
    greeting: string;
    constructor(message: string) { }
    greet() { return 'there'; }
    // 允許多不允許少(鴨子類型)
    sayHello() { /*...*/ }
}

let greeterType: typeof Greeter = GreeterDuck;

更進一步的:

// 從 Class 類型擴展出 Interface
interface HelloGreeter extends Greeter {
    sayHello(): void;
}

let hGreeter: HelloGreeter = new GreeterDuck('world');

沒錯,因為類具有類型含義,所以接口能夠繼承自類

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論