4.1 類理論
面向?qū)ο缶幊虖?qiáng)調(diào)的是數(shù)據(jù)和操作數(shù)據(jù)的行為本質(zhì)上是互相關(guān)聯(lián)的,因此好的設(shè)計(jì)就是把數(shù)據(jù)以及和它相關(guān)的行為打包起來。有說這被稱為數(shù)據(jù)結(jié)構(gòu)。
舉例,用來表示一個(gè)單詞或者短語的一串字符通常被稱為字符串。字符就是數(shù)據(jù)。但是你關(guān)心的往往不是數(shù)據(jù)是什么,而是可以對(duì)數(shù)據(jù)做什么,所以可應(yīng)用在這種數(shù)據(jù)上的行為都被設(shè)計(jì)成String類的方法。
類理論強(qiáng)烈建議父類和子類使用相同的方法名來表示特定的行為,從而讓子類重寫父類。在JS中這樣做會(huì)降低代碼的可讀性和健壯性。
4.1.2 JS中的 “類”
雖然有近似類的語法,但是JS的機(jī)制似乎一直在阻止你使用類設(shè)計(jì)模式。在近似的表象之下,JS的機(jī)制其實(shí)和類完全不同。語法糖和JS類庫試圖掩蓋這個(gè)現(xiàn)實(shí),但是你遲早會(huì)面對(duì)它:其他語言中的類和JS中的“類”并不一樣。
4.2 類的機(jī)制
在許多面向類的語言中,“標(biāo)準(zhǔn)庫”會(huì)提供Stack類,它是一種“?!睌?shù)據(jù)結(jié)構(gòu)(支持壓入,彈出,等等)。Stack類內(nèi)部會(huì)有一些變量來存儲(chǔ)數(shù)據(jù),同時(shí)會(huì)提供一些公有的可訪問行為(方法),從而讓你的代碼可以和(隱藏的)數(shù)據(jù)進(jìn)行交互(比如添加,刪除數(shù)據(jù))。
4.2.1 建造
“類”和“實(shí)例”的概念來源于房屋建造。
建筑和藍(lán)圖之間的關(guān)系是間接的。一個(gè)類就是一張藍(lán)圖。為了獲得真正可交互的對(duì)象,我們必須按照類來建造(實(shí)例化)一個(gè)東西,這個(gè)東西通常被稱為實(shí)例,有需要的話,可直接在實(shí)例上調(diào)用方法并訪問其所有公有數(shù)據(jù)屬性。
這個(gè)對(duì)象就是類中描述的所有特性的一份副本。
把類和實(shí)例對(duì)象之間的關(guān)系看作是直接關(guān)系而不是間接關(guān)系通常更好理解,類通過復(fù)制操作被實(shí)例化為對(duì)象形式
4.2.2 構(gòu)造函數(shù)
類實(shí)例是由一個(gè)特殊類方法構(gòu)造的,這個(gè)方法名通常和類名相同,這個(gè)方法的任務(wù)就是初始化實(shí)例需要的所有信息。
類構(gòu)造函數(shù)屬于類,通常和類同名。此外,構(gòu)造函數(shù)大多需要new來調(diào),這樣語言引擎才知道你想要構(gòu)造一個(gè)新的類實(shí)例。
4.3 類的繼承
在面向類的語言中,可先定義一個(gè)類,然后定義一個(gè)繼承前者的類。
后者稱為“子類”,前者稱為“父類”。
定義好一個(gè)子類后,相對(duì)于父類來說它就是一個(gè)獨(dú)立且完全不同的類。子類會(huì)包含父類行為的原始副本,但是也可重寫所有繼承的行為甚至定義新行為。
非常重要的一點(diǎn)是,我們討論的父類和子類并不是實(shí)例。
我們可把父類和子類稱為父類DNA和子類DNA。我們需要根據(jù)這些DNA來創(chuàng)建(或?qū)嵗┮粋€(gè)人,然后才能和他進(jìn)行溝通。
class Vehicle {
engines = 1
ignition() {
output("Turning on my engine.");
}
drive() {
ignition();
output("Steering and moving forward!")
}
}
class Car inherits Vehicle {
wheels = 4;
drive() {
inherited: drive()
output( "Rolling on all", wheels, "wheels!" )
}
}
class SpeedBoat inherits Vehicle {
engines = 2
ignition() {
output( "Turning on my", engines, " engines" )
}
pilot() {
inherited: drive()
output ("Speeding through the water with ease!")
}
}
我們定義Vehicle類來假設(shè)一種發(fā)動(dòng)機(jī),一種點(diǎn)火方式,一種駕駛方法。但你不可能制造一個(gè)通用的“交通工具”,因?yàn)檫@個(gè)類只是一個(gè)抽象的概念。
接下來定義了兩類具體的交通工具:Car 和 SpeedBoat 。它們都從 Vehicle 繼承了通用的特性并根據(jù)自身類別修改了某些特性。汽車需要四個(gè)輪子,快艇需要兩個(gè)發(fā)動(dòng)機(jī)
4.3.1 多態(tài)
Car 重寫了繼承自父類的drive() 方法,但之后Car調(diào)用了繼承的inherited:drive() 方法,這表明Car 可引用繼承來的原始drive()方法。快艇的pilot() 方法同樣引用了原始drive() 方法。
這個(gè)技術(shù)被稱為多態(tài)或者虛擬多態(tài),或相對(duì)多態(tài)。
多態(tài)并不表示子類和分類有關(guān)聯(lián),子類得到的只是父類的一份副本。類的繼承其實(shí)就是復(fù)制。
4.3.2 多重繼承

這個(gè)機(jī)制帶來了復(fù)雜的問題,上例中如果A中有drive() 并且B 和 C 都重寫了這個(gè)方法(多態(tài)),那D應(yīng)當(dāng)選擇哪個(gè)版本呢?
相比之下,JS要簡(jiǎn)單得多:它本身并不提供“多重繼承”。但開發(fā)者嘗試各種辦法來實(shí)現(xiàn)多重繼承,后面會(huì)看到。
4.4 混入
在繼承或者實(shí)例化時(shí),JS的對(duì)象機(jī)制并不會(huì)自動(dòng)執(zhí)行復(fù)制行為。簡(jiǎn)單來說,JS中只有對(duì)象,并不存在可被實(shí)例化的“類”。一個(gè)對(duì)象并不會(huì)被復(fù)制到其他對(duì)象,它們會(huì)被關(guān)聯(lián)起來。
由于在其他語言中類表現(xiàn)為復(fù)制行為,因此JS開發(fā)者模擬出了復(fù)制行為,就是混入。后面有兩種類型的混入:顯式和隱式。
4.4.1 顯式混入
由于JS不會(huì)自動(dòng)實(shí)現(xiàn)前例Vehicle到Car的復(fù)制行為,所有我們需要手動(dòng)實(shí)現(xiàn)復(fù)制功能。這個(gè)功能在許多庫和框架中被稱為extend(),但是為了方便理解我們稱為mixin()。
function mixin(sourceObj, targetObj){
for (var key in sourceObj) {
// 只會(huì)在不存在的情況下復(fù)制
if(!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
var Vehicle = {
engines: 1,
ignition() {
console.log("Turning on my engine");
},
drive(){
this.ignition();
console.log("Steering and moving forward!");
}
};
var Car = mixin( Vehicle, {
wheels: 4,
drive(){
Vehicle.drive.call(this);
console.log("Rolling on all " + this.wheels + " wheels!");
}
});
有一點(diǎn)需要注意,我們處理的已經(jīng)不再是類了,因?yàn)樵贘S中不存在類,Vehicle 和 Car 都是對(duì)象,供我們分別進(jìn)行復(fù)制和粘貼。
現(xiàn)在Car 中就有了一份Vehicle屬性和函數(shù)的副本了。從技術(shù)角度來說,函數(shù)實(shí)際上沒有被復(fù)制,復(fù)制的是函數(shù)引用。所以,Car中的屬性ingition只是從Vehicle 中復(fù)制過來的對(duì)于ingition()函數(shù)的引用。相反,屬性engines就是直接從Vehicle中復(fù)制了值1.
1.再說多態(tài)
分析這條語句Vehicle.drive.call(this)。這就是顯式多態(tài)。在之前的偽代碼中對(duì)應(yīng)的語句是inherited:drive(),我們稱之為相對(duì)多態(tài)。
ES6之前,沒有想對(duì)多態(tài)的機(jī)制。由于Car和Vehicle中都有drive() 函數(shù),為了指明調(diào)用對(duì)象,我們必須使用絕對(duì)引用。通過名稱顯式指定Vehicle對(duì)象并調(diào)用它的drive() 函數(shù)。
但是如果直接執(zhí)行Vehicle.drive(),函數(shù)調(diào)用中的this會(huì)被綁定到Vehicle對(duì)象而不是Car對(duì)象,這并不是我們想要的。因此,使用.call(this)
如果函數(shù)Car.drive() 的名稱標(biāo)識(shí)符并沒有和Vehicle.drive()重疊(屏蔽)的話,我們就不需要實(shí)現(xiàn)方法多態(tài),因?yàn)檎{(diào)用mixin()時(shí)會(huì)把函數(shù)Vehicle.drive()的引用復(fù)制到Car中,因此我們可直接訪問this.drive()。正是由于存在標(biāo)識(shí)符重疊,所有必須使用更加復(fù)雜的顯式多態(tài)方法。
在支持相對(duì)多態(tài)的面向類的語言中,Car和Vehicle之間的聯(lián)系只在類定義的開頭被創(chuàng)建,從而只需要在這一個(gè)地方維護(hù)兩個(gè)類的聯(lián)系。
但是在JS中(由于屏蔽)使用顯式偽多態(tài)會(huì)在所有需要使用(偽)多態(tài)引用的地方創(chuàng)建一個(gè)函數(shù)關(guān)聯(lián),這會(huì)極大地增加維護(hù)成本。
使用偽多態(tài)通常會(huì)導(dǎo)致代碼變得更加復(fù)雜,難以閱讀并且難以維護(hù),因此應(yīng)當(dāng)盡量避免使用顯式偽多態(tài),因?yàn)檫@樣做往往得不償失。
2.混合復(fù)制
回顧mixin() 函數(shù),它會(huì)遍歷sourceObj的屬性,如果在targetObj(本例是Car)沒沒有這個(gè)屬性就會(huì)進(jìn)行復(fù)制。由于我們是在目標(biāo)對(duì)象初始化之后才進(jìn)行復(fù)制,因此一定要小心不要覆蓋目標(biāo)對(duì)象的原有屬性。
如果我們先進(jìn)行復(fù)制然后對(duì)Car進(jìn)行特殊化的話。就可跳過存在性檢查。不過并不好用且效率低:
// 另一種混入函數(shù),可能有重寫風(fēng)險(xiǎn)
function mixin(sourceObj, targetObj) {
for(var key in sourceObj) {
targetObj[key] = sourceObj[key];
}
return targetObj;
}
var Vehicle = {
//...
}
// 首先創(chuàng)建一個(gè)空對(duì)象并把Vehicle的內(nèi)容復(fù)制進(jìn)去
var Car = mixin(Vehicle, {});
//然后把新內(nèi)容復(fù)制到Car中
mixin({
wheels: 4,
drive(){
// ...
}
}, Car)
這兩種方法都可把不重疊的內(nèi)容從Vehicle中顯性復(fù)制到Car中。由于兩個(gè)對(duì)象引用的是同一個(gè)函數(shù),因此這種復(fù)制實(shí)際上并不能完全模擬面向類的語言中的復(fù)制。
JS中的函數(shù)無法真正的復(fù)制,所以你只能復(fù)制對(duì)共享函數(shù)對(duì)象的引用。
一定要注意,只在能提高代碼可讀性的前提下使用顯式混入,避免使用增加代碼理解難度或讓對(duì)象關(guān)系更復(fù)雜的模式。
如果使用混入時(shí)感覺越來越困難,那或許你應(yīng)該停止使用它了。
3.寄生繼承
顯式混入的一種變種被稱為“寄生繼承”。即是顯式又是隱式的。
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!");
}
// 寄生類 Car
function Car() {
// 首先,car是一個(gè)Vehicle
var car = new Vehicle();
// 接著我們對(duì)car 進(jìn)行定制
car.wheels = 4;
// 保存到Vehicle::drive() 的特殊引用
var vehDrive = car.drive;
// 重寫 Vehicle::drive();
car.drive = function() {
vehDrive.call(this);
console.log("Rolling on all " + this.wheels + " wheels!");
}
return car;
}
var myCar = new Car();
myCar.drive();
4.4.2 隱式混入
隱式混入和之前的顯式偽多態(tài)很像,所有也具備相同的問題。
var Something = {
cool(){
this.greeting = "Hello World!"=;
this.count = this.count ? this.count + 1 : 1;
}
}
var Another = {
cool(){
//隱式把Someing混入Another
Something.cool.call(this);
}
}
雖然這類技術(shù)利用了this的重新綁定功能,但是Something.cool.call(this)仍然無法變成相對(duì)引用,所以使用時(shí)千萬小心。應(yīng)避免使用這樣的結(jié)構(gòu)。