一周一章前端書·第9周:《你不知道的JavaScript(上)》S02E04

第4章 :混合對象“類”

  • 本章要介紹和類相關的面相對象編程的知識。首先會介紹面相對象類的設計模式:實例化(instantiation)、繼承(inheritance)、多態(polymorphism)。
  • 但由于這些概念上無法直接對應到JavaScript的對象機制,因此很多JavaScript開發者使用了一些比如mixin等解決方法來實現。

4.1 類理論

  • 類/繼承,或者說面向對象,是用來描述代碼的一種組織結構形式,它是一種軟件對真實世界的建模方法。
  • 面向對象編程強調的是數據和操作數據的行為,本質是是相互關聯的。因此,面相對象編程推崇的是,把數據以及和它相關的行為封裝起來。這在正式的計算機科學中被稱為數據結構。
  • 舉例:我們在編程中要描述一個數據,然后我們會定義一個變量,這個值是一個字符串,這也就是數據。但我們在使用的過程之,往往關心的不是數據是什么,而是可以對數據做什么,也就是應用再數據上的行為(比如計算數據長度啊、追加數據啊、搜索數據中的字符啊等等)。因此這些行為被設計成了String類。
  • 再來看一個例子:“汽車”可以被看做“交通工具”的一種,對于這種關系,我們在軟件中可以定義Vehicle類和Car類來進行建模。
  • Vehicle類的定義,可能包含推進器(比如引擎)、載人能力等。它包含所有類型的交通工具,不管是飛機、汽車或者火車。
  • 而Car類繼承了Vehicle類,以通用的Vehicle類作為基礎進行特殊化定義,比如加上汽車的車輛識別碼等。
  • 雖然Vehicle和Car會定義相同的方法,但實例中的數據可能是不同的,這就是類、繼承和實例化。
  • 類的另一個核心概念是多態,是說父類的通用行為可以被子類用更特殊的行為進行重寫。
4.1.1 “類”設計模式
  • 有人可能從來沒把類作為設計模式來看,反而更熟悉的是比如觀察者模式、工廠模式、單例模式等其他模式。但是這些高級設計模式,都以面向對象類的基礎上實現的。
  • 可能你還聽過“過程化編程”,這種代碼只包含過程調用,沒有高層的抽象。
  • 除此之外,如果有函數式編程的開發經驗,會知道類也是非常常用的設計模式,但類不是必須的編程基礎,而是一種可選的代碼抽象。而在其他編程語言比如Java中,并不給你選擇的機會,萬物皆對象。
4.1.2 JavaScript中的“類”
  • 在軟件設計中,類是一種可選的模式。由于許多開發者都非常喜歡面向類的軟件設計,JavaScript提供了語法糖來滿足對于類設計模式的最普片需求,比如ES6新增了比如class關鍵字等,但javascript中的“類”和和其他語言中的類并不一樣。

4.2 類的機制

  • 在許多面向類的語言中,內置會提供Stack類,這是一種“棧”數據結構,同時會提供一些公有的方法。但實際上我們并不是直接操作Stack,Stack類僅僅是一個抽象的表示,它本身并不是一個“棧”。使用的時候,通常也需要先實例化Stack類,然后才能對它進行操作。
4.2.1 建造
  • 類和實例的概念源于房屋建造。
    1. 在建造之前,建筑師通常會規劃一個建造藍圖來描繪建筑的特性:寬、高、幾室幾廳、多少個窗戶以及窗戶的位置等。但在這個階段,并不關心建筑會被建在哪,也不關心會建造多少個這樣的建筑,甚至不用關心建筑中的內容(比如家具、壁紙、吊燈等)。建筑藍圖只是建筑計劃,它們并不是真正的建筑。
    2. 接下來,需要一個建筑工人按照藍圖建造建筑,把規劃好的特性從藍圖中復制到現實世界的建筑中。
    3. 完成后,建筑就成為了藍圖的物理實例。如果需要建筑多套房子,只需把工作都重復一遍,再創建一份副本。
  • 建筑和藍圖之間的關系是間接的。藍圖只是抽象描繪了建筑的結構,比如藍圖只表示門在哪,但并不是真正的門,如果想打開一扇門,那就必須接觸真實的建筑才行。
  • 一個類就是一張建造藍圖,為了獲得真正的建筑,我們必須按照類來建造(實例化)一個東西,而這個東西通常被稱為實例。
4.2.2 構造函數
  • 實例是由一個特殊的類方法構造的,這個類方法的方法名通常和類名相同,稱為構造函數。
  • 構造函數大多情況下,需要用new來調用,構造函數的主要任務,就是初始化實例需要的所有信息。

4.3 類的繼承

  • 在面向類的語言中,當一個類繼承另一個類時。后者通常被稱為“子類”,前者會被稱為“父類”。
  • 從術語來看,顯然是類比父母和孩子。在現實中,孩子會從父母繼承許多基因特性,但通常孩子不會和父母一模一樣,即便外貌長相會類似,但性格行為卻大不相同,因為孩子是一個獨一無二的存在。
  • 同理,在程序中,相對于父類來說,子類也是一個獨立個體。子類會包含父類行為的原始副本,但也可以重寫所有繼承的行為甚至定義新的行為。

注意:有必要說明一下,這里說的父類和子類不是實例,而是像上文說得建造藍圖一樣。我們應當把父類和子類稱為父類DNA和子類DNA,需要根據這些DNA來創建(實例化)一個人,我們才擁有一個真實的實例。

4.3.1 多態
  • 當子類繼承父類,引用父類原始的方法時,這里被稱為多態(相對多態)。

說明:實際上多態是任何方法都能引用繼承層級中上層的類的方法。之所以說是“相對”,是因為當前子類引用的是父類的方法,而實際上還可以引用祖先類(superclass)的方法。

  • 多態并不表示子類和父類有關聯,子類得到的只是父類的一個副本。類的繼承其實就是復制。
4.3.2 多重繼承
  • 有些面向類的語言允許子類繼承多個“父類”,多重繼承意味著所有父類的定義都會被復制到子類中。
  • 從表面看,多繼承可以把許多功能組合在一起。然而這個機制也會帶來很多復雜的問題。比如,當子類調用兩個父類同名的方法時,就不知道該怎么處理了。
  • JavaScript本身也不提供“多重繼承”功能,但也可以用各種辦法來實現多重繼承。

4.4 混入

  • 在繼承或實例化時,JavaScript的對象機制并不會自動執行復制行為。換句話說,JavaScript中只有對象,不存在可以被實例化的“類”。不會復制對象,它們只是被關聯起來。
  • 由于在其他語言中,類表現出來的都是復制行為,因此JavaScript開發者也想出了一個方法來模擬類的復制行為,就是混入。
  • 有兩種類型的混入:顯式和隱式。
4.4.1 顯式混入
  • 之前提到的Vehicle和Car。接下來,我們用JavaScript手動實現復制功能。這個功能在許多庫和框架中被稱為extend(),但是為了方便理解我們稱之為minxin()
function minxin(sourceOBj,targetObj){
    for(var key in sourceObj){
        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!'
        );
    }
};
  • Car中就擁有一份Vehicle屬性和函數的副本了,但注意:復制的是函數的引用。
  • 再說多態:Vehicle.drive.call(this)就是顯示多態,清楚證明調用的是父類還是祖先類;inherited:drive()就是相對多態。
  • mixin()的工作原理:它會遍歷sourceObj的屬性,如果在targetObj沒有這個屬性就會進行復制。
  • 顯式混入模式的另一種變體被稱為“寄生繼承”:
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!');
};
function Car(){
    var car = new Vehicle();
    car.wheels = 4;
    var vehDrive = car.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 隱式混入
  • 隱式混入和顯式偽多態很像,但同樣擁有同樣的問題:不是真正的對象復制,而是復制引用。
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
Another = {
    cool : function(){
        Something.cool.call(this);
    }
};
Another.cool();
Another.greeting;   // 'Hello World'
Another.count;  // 1
  • 如果在構造函數調用使用Something.cool.call(this),最終的結果是Something.cool()中的賦值操作都會應用再Another對象上而不是Something對象上。
  • 通常來說,應該盡量避免使用這一的結構,以保證代碼的整潔和可維護性。

4.5 小結

  • 類是一種設計模式,許多語言提供了面向類軟件設計的原生語法。JavaScript也有類似的語法,但和其他語言中的類完全不同。
  • 類意味著復制。傳統的類被實例化的時候,它的行為會被復制到實例中。類被繼承時,行為也會被復制到子類中。
  • 多態看起來似乎是從子類引用父類,但本質上引用的其實是復制的結果。
  • JavaScript不會像類那樣自動創建對象的副本。但通過混入模式可以模擬類的復制行為,但通常會產生丑陋且脆弱的語法,這會讓代碼更加難懂并且難以維護。
  • 顯式混入實際上無法完全模擬類的復制行為,因為對象只是復制引用,無法復制被引用的對象或者函數本身。
  • 總的來說,在JavaScript中模擬類是得不償失的,可能會買下更多隱患。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容