第二部分 類

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 多重繼承

211526478759_.pic.jpg

這個(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)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容