Ch 9. 混合的「類別」物件


類別理論

OO 或類別導向所強調的是,資料本身就具有關聯行為能夠作用在它們身上,所以要經過適當的設計,將資料及行為打包在一起。

舉例來說,一串字元被稱為字串 (String),字元就是資料,而套用到資料的行為 (計算長度、搜尋、切分等等) 都被設計為 String 類別的方法。任何給定的字串都只是類別的一個實體 (instance)。

最常見的例子,一部車子或是火車可以被視為某個類別的具體實作,例如 Vehicle (載具)。車子與火車會有共通的屬性或行為,例如輪子、推進。而在軟體中,為不同類型的載具重複定義基本要素,並不合理。因此我們會在 Vehicle 上定義一次,當定義 Car 的時候,再繼承或擴充 Vehicle。

另一個關鍵概念是多型 (polymorphism),來自父類別的行為可以在子類別中被覆寫 (overridden),以賦予它特化的行為。特定行為應該共用方法名稱,如此才能覆寫。

「類別」設計模式

常見的熱門討論是 OO 的設計模式,這樣的情況下,幾乎是假設了 OO 是用來實現設計模式的機制,是程式的基礎。然而類別只不過是數種常見的設計模式之一

JavaScript「類別」

JS 具有 newinstanceofclass 等關鍵字已經好一陣子了,但它實際上並不具有類別。只是試著滿足「想要以類別進行設計」這種渴望而提供的語法。

建置

「類別 (class)」與「實體 (instance)」的思維,源自於建築工程。建築師會規畫建築物的所有特徵,但他並不在意建築物會在何處建構多少棟。他也不在意建築物的內容,只在意其中的結構。之後,我們會透過建造商將藍圖實現,製造出複製品,而且可以移動到隔壁的空地,再建造出另一個複製品。

類別就是藍圖,而實際能夠與之互動的物件,則是實體。

建構器

建構器是一種特殊的方法 (function),負責初始化實體會需要的任何資訊。一個類別的建構器屬於該類別,而且總是與該類別同名。使用 new 來建構時會同時呼叫建構器。

類別的繼承

在類別導向的語言中,不只能夠定義類別,還可以定義繼承自其他類別的類別。繼承來的類別通常被稱為「子類別 (child class)」,被繼承的則是「父類別 (parent class)」。當子類別定義完畢,它就會與父類別分離,成為不同的類別。子類別會含有從父類別複製過來的最初的行為,但在之後可以覆寫 (override)。

P.150 範例。Car 和 SpeedBoat 都繼承了 Vehicle 的通用特徵,但特化出了適合自己種類的特徵。

多型

多型是指任何方法都能參考到較高階層中的另一個方法。如 Car 定義了自己的 drive() 方法,但它還是能參考到它所繼承的 (Vehicle) 的 drive()。至於會採用哪個版本的方法,會根據你所參考的實體是哪個類別而來決定。

多重繼承

經典的鑽石問題,當一個子類別繼承了兩個父類別,當其參考一個方法時,要採用哪個版本呢?在 JS 之中,並沒有提供原生機制來進行多重繼承。

Mixins

物件並不會在繼承或實體化時自動複製行為,物件只會被連結在一起。開發人員可以透過 Mixins 來偽造複製的動作。

明確的 Mixins

// vastly simplified `mixin(..)` example:
function mixin( sourceObj, targetObj ) {
    for (var key in sourceObj) {
        // only copy if not already present
        if (!(key in targetObj)) {
            targetObj[key] = sourceObj[key];
        }
    }
    return targetObj;
}
var Vehicle = {
    engines: 1,
    ignition: function() {
        console.log( "Turning on my engine." );
    },
    drive: function() {
        this.ignition();
        console.log( "Steering and moving forward!" );
    }
};
var Car = mixin( Vehicle, {
    wheels: 4,
    drive: function() {
        // 多型
        Vehicle.drive.call( this );
        console.log( "Rolling on all " + this.wheels + " wheels!" );
    }
});

透過 Mixins 的方式,可以將兩個物件混合模擬出繼承和多型的行為,然而實際上共用的函式物件仍只是一個參考,如果修改了函式物件,例如 ignitionVehicleCar 都會受到影響。

寄生式繼承 (Parasitic Inheritance)

明確 Mixin 模式的一種變體。

// "Traditional JS Class" `Vehicle`
function Vehicle() {
    this.engines = 1;
}
Vehicle.prototype.ignition = function() {
    console.log( "Turning on my engine." );
};
Vehicle.prototype.drive = function() {
    this.ignition();
    console.log( "Steering and moving forward!" );
};
// "Parasitic Class" `Car`
function Car() {
    // first, `car` is a `Vehicle`
    var car = new Vehicle();
    // now, let's modify our `car` to specialize it
    car.wheels = 4;
    // save a privileged reference to `Vehicle::drive()`
    var vehDrive = car.drive;
    // override `Vehicle::drive()`
    car.drive = function() {
        vehDrive.call( this );
        console.log( "Rolling on all " + this.wheels + " wheels!" );
    };
    return car;
}
var myCar = new Car();
myCar.drive();
// Turning on my engine.
// Steering and moving forward!
// Rolling on all 4 wheels!

隱含的 Mixins

var Something = {
    cool: function() {
        this.greeting = "Hello World";
        this.count = this.count ? this.count + 1 : 1;
    }
};
Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1
var Another = {
    cool: function() {
    // implicit mixin of `Something` to `Another`
        Something.cool.call( this );
    }
};
Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1 (not shared state with `Something`)

藉由 Something.cool.call( this ); 利用 this 在 Another 中混進了 Something 的行為,因為隱含指定的緣故,在維護上變得較不清楚,建議盡可能避免。

複習

  • 類別意味著拷貝。傳統的類別被實體化時,會發生拷貝行為,多型看似參考,但實際上也是拷貝。
  • 然而,JS 中並沒有類別,都只是參考來將物件連結在一起。
  • Mixin 模式時常被用來模擬拷貝行為,然而物件及函式仍只是複製共用的參考。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *