類別理論
OO 或類別導向所強調的是,資料本身就具有關聯行為能夠作用在它們身上,所以要經過適當的設計,將資料及行為打包在一起。
舉例來說,一串字元被稱為字串 (String),字元就是資料,而套用到資料的行為 (計算長度、搜尋、切分等等) 都被設計為 String 類別的方法。任何給定的字串都只是類別的一個實體 (instance)。
最常見的例子,一部車子或是火車可以被視為某個類別的具體實作,例如 Vehicle (載具)。車子與火車會有共通的屬性或行為,例如輪子、推進。而在軟體中,為不同類型的載具重複定義基本要素,並不合理。因此我們會在 Vehicle 上定義一次,當定義 Car 的時候,再繼承或擴充 Vehicle。
另一個關鍵概念是多型 (polymorphism),來自父類別的行為可以在子類別中被覆寫 (overridden),以賦予它特化的行為。特定行為應該共用方法名稱,如此才能覆寫。
「類別」設計模式
常見的熱門討論是 OO 的設計模式,這樣的情況下,幾乎是假設了 OO 是用來實現設計模式的機制,是程式的基礎。然而類別只不過是數種常見的設計模式之一。
JavaScript「類別」
JS 具有 new
、instanceof
、class
等關鍵字已經好一陣子了,但它實際上並不具有類別。只是試著滿足「想要以類別進行設計」這種渴望而提供的語法。
建置
「類別 (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 的方式,可以將兩個物件混合模擬出繼承和多型的行為,然而實際上共用的函式物件仍只是一個參考,如果修改了函式物件,例如 ignition
,Vehicle
和 Car
都會受到影響。
寄生式繼承 (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 模式時常被用來模擬拷貝行為,然而物件及函式仍只是複製共用的參考。