最近更新: 2016-03-10

學習 ECMAScript/JavaScript 6 - Class

本文將介紹 ECMAScript/JavaScript 6 新式的 Class (類別) 語法。但直到本文發布時,實作支持此語法的瀏覽器還不多,多數還在測試階段。想要練習本文內容,你需要使用 JavaScript compiler ,例如 Babel,將包含 ES6 語法的 JavaScript 程式碼,轉譯為現在的瀏覽器認得的語法。

2016年3月10日正式發佈的 Firefox 45 已支持 Class 語法。

ECMAScript/JavaScript 之前雖然沒有提供 class 語法,但它仍然是一種個體導向程式語言(OOPL),只不過它屬於 prototype-based 這支比較冷門的派別。 prototype-based 這支的世界觀比較接近現實世界,因為在現實世界中是先有獨特的個體出現,我們才以觀察者的身份依共同特徵將複數個體分類,這才有類別。ECMAScript/JavaScript 仍然支援繼承 (inheritance) 、封裝 (encapsulation) 、動態連結 (dynamic binding) 這三種特性。

為何增加 class 語法

儘管 ECMAScript/JavaScript 的封裝繼承規範一樣不缺,但目前程式語言的主要派別是 class-based ,先定義類別,才有實例的個體。很多編程人員一開始學的就是 class-based 派的程式語言。當他們開始接觸 JavaScript 時,通常腦子轉不過來,因 JavaScript 沒有 class, extends, constructor 這些關鍵字而困惑,抱怨 JavaScript 的語法不完善。所以 ES6 決定從眾,增加了 class 語法。

Class 語法是 ECMAScript/JavaScript 原有個體封裝與繼承模式的另一種包裝糖衣。這個糖衣雖然少了一些設計彈性,但好處是語意結構比較嚴謹易讀。對於老練的 JavaScript 編程人員來說,學不學無所謂。但有助於降低使用者從其他程式語言轉換過來的門檻。

我們先複習一次 ECMAScript/JavaScript 3 就已經存在的「傳統」JavaScript 封裝與繼承用法,這部份請看我以前寫的兩篇文章:

Property 定義

轉眼間,這兩篇文章已經過十年了。在這段時間中, C# 的普及讓人們認識了 property 的使用彈性。所以未正式推廣的 ECMAScript/JavaScript 5 規範增加了 defineProperty() 方法,讓編程人員可以自定個體屬性的存取方法 (setter/getter)。請看下列示範。

function Square(width, height)
{
    var _width = width;     // private
    var _height = height;

    // defineProperty() 第2個參數,是屬性名稱(字串)
    // 第3個參數是屬性存取方法表, get 定義取值方法(getter), set 定義存值方法(setter)
    Object.defineProperty(this, "width", {
        get: function() {
            console.log("取 getter")
            return _width;
        },
        set: function(v) {
            console.log("存 setter", v);
            _width = v;
        }
    });

    Object.defineProperty(this, "area", {
        get: function() {
            console.log("取面積")
            return _width * _height;
        },
        set: function(v) { // DO NOTHING. 這段定義可省略不寫.
            throw new Error("this property is read only");
        }
    });
}

var sq = new Square(10,20);
console.log(sq.area);
sq.width = 15;
console.log(sq.area);
sq.area = 100 // trhow error.

上例的 defineProperty() 可能有些較舊的瀏覽器並未實作。但隨著 ES6 的落實,以後支持 ES6 的瀏覽器也會提供 defineProperty()

Class 語法

ECMAScript/JavaScript 6 新增的 Class 語法,基本上就和其他程式語言的用法差不多。相關的關鍵字有 class, extends, constructor, super, static, get, set 。但未提供 public, private 等存取權限飾詞。

將上例的 Square 以新的 Class 語法改寫如下:

class Square
{
    constructor(width, height) {
        this._width = width;  // 資料成員在建構方法內初始化。存取權限為 public
        this._height = height;
    }

    get width() { // property getter
        return this._width;
    }

    set width(v) { // property setter
        this._width = v;
    }

    get area() {
        return this._width * this._height;
    }
}

var sq = new Square(10,20);
console.log(sq.area);
sq.width = 15;
console.log(sq.area);

在 Class 中定義方法內容時,不需要加上 function 關鍵字。建構方法 constructor() 則是類別實體化時調用的方法,負責設置實體的初始內容。至於 Property 的定義方式也很簡單,只要在方法前面加上 getset 飾詞即可。

若要衍生新的類別,使用 extends 指示新的類別繼承的基底類別。而 super 則是在衍生類別中用於指示基底類別的關鍵字;super.??? 表示基底類別成員,super() 表示調用基底類別的建構方法。在方法名稱前加上 static 飾詞則是定義類別方法。這些用法都和其他 class-based 的程式語言差不多。

下列是衍生類別的範例:

class Animal
{
    constructor(name) {
        console.log("animal constructor");
        this.name = name;
    }

    speak() {
        console.log(this.name + ' speaks');
    }
}

class Dog extends Animal
{
    constructor(name) {
        super(name);  // invoke constructor of base class.
        this.foot = 4;
    }

    speak() {   // method overriding
        console.log(this.name + ' barks');
    }
}

ECMAScript/JavaScript 6 特有內容

除了和其他程式語言相同之處, ECMAScript/JavaScript 6 還是有一些特有的內容。

Computed property names

在 ECMAScript/JavaScript 6 中,你可以在成員名稱的位置用 [ ] 包住一段敘述,將敘述的計算結果作為成員名稱。敘述可以是變數、函數或符號。這種用法稱為 “Computed property names”。這用法不限於配合新的 Class 語法,其他地方也可以用。

範例:

var Example = class {
    ['a'+1](args) {
        //...
    }

    [one_function()](args) {
        //...
    }

    [Symbol()](args) {
        //...
    }
}

var xyz = {
    ['T'+Date.now()]: 1
}
Generator

在方法名稱前加上 * 號,就是將此方法定義為 generator

new.target

在衍生類別中,你可以透過 super 操作基底類別成員。那麼反過來呢? ECMAScript/JavaScript 6 就增加了 new.target 讓你可以在基底類別的方法中,得知衍生類別為何。

一個類別可以衍生好幾個類別,所以 new.target 的實際指涉對象會改變。它是看 new 操作符的對象決定。每一個函數內部都會隱含 new.target 變數 (就像 arguments);若這個函數不是透過 new 調用的話,其值將為 undefined

注意,在良好的繼承樹中,基底類別不必認知衍生類別的內容,所以要小心,別誤用了 new.target

相關文章