JavaScript 和 TypeScript 中的 class

misaky發表於2018-04-19

對於一個前端開發者來說,很少用到 class ,因為在 JavaScript 中更多的是 函式式 程式設計,抬手就是一個 function,幾乎不見 class 或 new 的蹤影。所以 設計模式 也是大多數前端開發者的一個短板。

最近在學習 Angular 的過程中發現其大量的運用了 class,不得不佩服,Angular 確實是一個優秀的、值得深入研究的 框架

本文將簡單的介紹一下 JavaScriptTypeScript 中的 class。

基本概念

在介紹 class 之前,要先介紹一些基本的概念。

  1. 靜態成員

    類自身的成員,可以繼承,但例項無法訪問,一般多見於工具類,比如在jQuery時代最常見的 $.ajaxajax 便是 $ 的靜態方法,使用方便,不需要再通過 new 或者函式呼叫的得到一個新例項。

  2. 私有成員

    類內部的成員,一般是不能繼承的,只能在內部使用,例項無法訪問,有一點點像閉包內部的變數,但是還是一定的差別,目前 JavaScript 無法直接定義私有成員,只能通過其它方式輔助實現。

  3. getter/setter

    存取器屬性,當我們訪問或者修改一個例項的屬性的時候,我們可通過存取器屬性攔截這兩個操作,從而做一些其它的事情,vue正是通過這個api來實現對資料變化的追蹤。

  4. 例項成員

    new 出來的例項所具有的成員,可以被繼承,也是通過這個特性實現了程式碼的複用。

  5. 抽象類,抽象方法

    抽象類指不可以被例項化的類,通過 new 關鍵字呼叫會報錯,一般都被設計成父類。

    抽象方法,只提供方法的名稱,引數和返回值,不負責實現,具體的實現由子類去完成,如果一個子類繼承於抽象類,那麼這個子類必須實現父類所有的抽象方法,否則會報錯。

    這兩個概念在 JavaScript 都無法直接實現,但在 TypeScript 或 其它面嚮物件語言中可以輕鬆實現,另外這個特性也是用於實現 多型 的重要手段。

案例介紹

為了更好的介紹 class,本文將採用三個 來做例子,分別是 PersonChineseAmerican。從字面上可以很快的知道: Person 是 父類(基類) ,Chinese 和 American 是 子類(派生類)

Person 有 name、age、gender 三個屬性,sayHello 方法和 fullName 存取器屬性。同時 Person 還有一些 靜態成員私有成員 ,由於實在太難想例子了,所以就用 foo、bar、x、y、z 這些來代替吧。

作為子類的 Chinese 和 American 繼承了 Person 的例項成員和靜態成員。同時它們自身也有一些自己的方法和屬性:

Chinese 有 kungfu 屬性,會習武 martial。

American 有 twitter,還可以 sendTwitter。

接下來我們就分別使用 JavaScript 和 TypeScript 來實現這個案例。

JavaScript 中的 class

JavaScript 中的 class 要分開說,在 ES6 中提供了兩個關鍵字 classextends ,雖然它們只是語法糖,底層還是再利用 prototype 實現繼承的,但是不能否認,這中寫法確實讓程式碼更清晰,更易讀。

ES6 中的 class

class Person {

    // #x = `私有屬性x`;
    // static x = `靜態屬性x`;
    // name;
    // age;
    // gender;

    // 上面的寫法還在提案中,並沒有成為正式標準,不過變化的可能性已經不大了。
    // 順便吐槽一下,用 # 表示私有成員,真的是很無語.

    /**
     * Person的靜態方法,可以被子類繼承
     * 可以通過 this 訪問靜態成員
     */
    static foo() {
        console.log(`類 ${this.name} 有一個 ${this.x}`);
    }

    constructor(name, age, gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    /**
     * 資料儲存器,可以訪問例項成員,子類的例項可以繼承
     * 以通過 this 訪問例項成員
     */
    get fullName() {
        const suffix = this.gender === `男` ? `先生` : `女士`;
        return this.name + suffix;
    }

    set fullName(value) {
        console.log(`你已改名為 ${value} `);
    }

    /**
     * Person的例項方法,可以被子類的例項繼承
     * 可以通過 this 訪問例項成員
     */
    sayHello() {
        console.log(`你好我是 ${this.fullName} ,我 ${this.age} 歲了`);
    }
}
Person.x = `靜態屬性x`;
class Chinese extends Person {

    static bar() {
        console.log(`類 ${this.name} 的父類是 ${super.name}`);
        super.foo();
    }

    constructor(name, age, gender, kungfu) {
        super(name, age, gender);
        this.kungfu = kungfu;
    }

    martial() {
        console.log(`${this.name} 正在修煉 ${this.kungfu} `);
    }
}
class American extends Person {

    // static y = `靜態屬性y`;

    static bar() {
        console.log(`類 ${this.name} 有自己的 ${this.y} ,還繼承了父類 ${super.name} 的 ${super.x}`);
    }

    constructor(name, age, gender, twitter) {
        super(name, age, gender);
        this.twitter = twitter;
    }

    sendTwitter(msg) {
        console.log(`${this.name} : `);
        console.log(`  ${msg}`);
    }
}
American.y = `靜態屬性y`;
Person.x;       // 靜態屬性x
Person.foo();   // 類 Person 有一個 靜態屬性x

Chinese.x;      // 靜態屬性x
Chinese.foo();  // 類 Chinese 有一個 靜態屬性x
Chinese.bar();  // 類 Chinese 的父類是 Person

American.x;     // 靜態屬性x
American.y;     // `靜態屬性y
American.foo(); // 類 American 有一個 靜態屬性x
American.bar(); // 類 American 有自己的 靜態屬性y ,還繼承了父類 Person 的 靜態屬性x

const p = new Person(`Lucy`, 20, `女`);
const c = new Chinese(`韓梅梅`, 18, `女`, `詠春拳`);
const a = new American(`川普`, 72, `男`, `Donald J. Trump`);

c.sayHello();   // 你好我是 韓梅梅女士 ,我 18 歲了
c.martial();    // 韓梅梅 正在修煉 詠春拳 
a.sayHello();   // 你好我是 川普先生 ,我 72 歲了
a.sendTwitter(`推特治國`);  // 川普 : 推特治國

ES6 之前的 class

ES5 的繼承,實質是先創造子類的例項物件 this,

然後再將父類的方法新增到 this 上面 Parent.apply(this) 。

ES6 的繼承機制完全不同,實質是先創造父類的例項物件 this,所以必須先呼叫 super 方法,

然後再用子類的建構函式修改this。

為了實現繼承,我們需要先實現一個 extendsClass 函式,它的作用是讓子類繼承父類的靜態成員和例項成員。

function extendsClass(parent, child) {

    // 防止子類和父類相同名稱的成員被父類覆蓋
    var flag = false;

    // 繼承靜態成員
    for (var k in parent) {
        flag = k in child;
        if (!flag) {
            child[k] = parent[k];
        }
    }

    // 繼承父類prototype上的成員
    // 用一個新的建構函式切斷父類和子類之間的資料共享
    var F = function () { }
    F.prototype = parent.prototype;
    var o = new F();
    for (var k in o) {
        flag = k in child.prototype;
        if (!flag) {
            child.prototype[k] = o[k];
        }
    }
}
function Person(name, age, gender) {
    this.name = name;
    this.age = age;
    this.gender = this.gender;
    // 如果將 getter/setter 寫在 prototype 會獲取不到
    Object.defineProperty(this, `fullName`, {
        get: function () {
            var suffix = this.gender === `男` ? `先生` : `女士`;
            return this.name + suffix;
        },
        set: function () {
            console.log(`你已改名為 ` + value + ` `);
        },
    });
}

Person.x = `靜態屬性x`;
Person.foo = function () {
    console.log(`類 ` + this.name + ` 有一個 ` + this.x);
}

Person.prototype = {
    constructor: Person,
    // get fullName() { },
    // set fullName(value) { },
    sayHello: function () {
        console.log(`你好我是 ` + this.fullName + ` ,我 ` + this.age + ` 了`);
    },
};
function Chinese(name, age, gender, kungfu) {
    // 用call改變this指向,實現繼承父類的例項屬性
    Person.call(this, name, age, gender);
    this.kungfu = kungfu;
}

Chinese.bar = function () {
    console.log(`類 ` + this.name + ` 的父類是 ` + Person.name);
    Person.foo();
}

Chinese.prototype = {
    constructor: Chinese,
    martial: function () {
        console.log(this.name + ` 正在修煉 ` + this.kungfu + ` `);
    }
};

extendsClass(Person, Chinese);
function American(name, age, gender, twitter) {
    Person.call(this, name, age, gender);
    this.twitter = twitter;
}

American.y = `靜態屬性y`;
American.bar = function () {
    console.log(`類 ` + this.name + ` 有自己的 ` + this.y + ` ,還繼承了父類 ` + Person.name + ` 的 ` + Person.x);
}

American.prototype = {
    constructor: American,
    sendTwitter: function (msg) {
        console.log(this.name + ` : `);
        console.log(`  ` + msg);
    }
};

extendsClass(Person, American);

TypeScript 中的 class

講完了 JavaScript 中的類,還是沒有用到 抽象類,抽象方法,私有方法這三個概念,由於 JavaScript 語言的侷限性,想要實現這三種概念是很困難的,但是在 TypeScript 可以輕鬆的實現這一特性。

首先我們稍微修改一下例子中的描述,Person 是抽象類,因為一個正常的人肯定是有國籍的,Person 的 sayHello 方法是抽象方法,因為每個國家打招呼的方式不一樣。另外一個人的性別是隻能讀取,不能修改的,且是確定的是,不是男生就是女生,所以還要藉助一下列舉。

enum Gender {
    female = 0,
    male = 1
};
abstract class Person {

    private x: string = `私有屬性x,子類和例項都無法訪問`;

    protected y: string = `私有屬性y,子類可以訪問,例項無法訪問`;

    name: string;
    public age: number;
    public readonly gender: Gender; // 用關鍵字 readonly 表明這是一個只讀屬性

    public static x: string = `靜態屬性x`;
    public static foo() {
        console.log(`類 ${this.name} 有一個 ${this.x}`);
    }

    constructor(name: string, age: number, gender: Gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    get fullName(): string {
        const suffix = this.gender === 1 ? `先生` : `女士`;
        return this.name + suffix;
    }

    set FullName(value: string) {
        console.log(`你已改名為 ${value} `);
    }

    // 抽象方法,具體實現交由子類完成
    abstract sayHello(): void;
}
class Chinese extends Person {

    public kungfu: string;

    public static bar() {
        console.log(`類 ${this.name} 的父類是 ${super.name}`);
        super.foo();
    }

    public constructor(name: string, age: number, gender: Gender, kungfu: string) {
        super(name, age, gender);
        this.kungfu = kungfu;
    }

    public sayHello(): void {
        console.log(`你好我是 ${this.fullName} ,我 ${this.age} 歲了`);
    }

    public martial() {
        console.log(`${this.name} 正在修煉 ${this.kungfu} `);
    }
}
class American extends Person {

    static y = `靜態屬性y`;

    public static bar() {
        console.log(`類 ${this.name} 有自己的 ${this.y} ,還繼承了父類 ${super.name} 的 ${super.x}`);
    }

    public twitter: string;

    public constructor(name: string, age: number, gender: Gender, twitter: string) {
        super(name, age, gender);
        this.twitter = twitter;
    }

    public sayHello(): void {
        console.log(`Hello, I am ${this.fullName} , I`m ${this.age} years old`);
    }

    public sendTwitter(msg: string): void {
        console.log(`${this.name} : `);
        console.log(`  ${msg}`);
    }
}
Person.x;       // 靜態屬性x
Person.foo();   // 類 Person 有一個 靜態屬性x

Chinese.x;      // 靜態屬性x
Chinese.foo();  // 類 Chinese 有一個 靜態屬性x
Chinese.bar();  // 類 Chinese 的父類是 Person

American.x;     // 靜態屬性x
American.y;     // `靜態屬性y
American.foo(); // 類 American 有一個 靜態屬性x
American.bar(); // 類 American 有自己的 靜態屬性y ,還繼承了父類 Person 的 靜態屬性x

const c: Chinese = new Chinese(`韓梅梅`, 18, Gender.female, `詠春拳`);
const a: American = new American(`川普`, 72, Gender.male, `Donald J. Trump`);

c.sayHello();   // 你好我是 韓梅梅女士 ,我 18 歲了
c.martial();    // 韓梅梅 正在修煉 詠春拳 
a.sayHello();   // Hello, I am 川普先生 , I`m 72 years old
a.sendTwitter(`推特治國`);  // 川普 : 推特治國

相關文章