新手看JS的六種繼承方式

??有了實體,為了簡化定義,自然就有了繼承的概念,比如已經定義了一個“人”類,后面需要詳細分出“男人”和“女人”,若是在后者的定義中再重述一邊“人”的相關屬性,就相當于重復的內容寫了三次,麻煩且多余,若是有一個方法,在已有類的基礎上,還能再根據要求添加新的屬性,便能極大地簡化效率,繼承的作用便是這樣!

原型和原型鏈

??繼承和原型鏈息息相關,關于面向對象和原型的定義,我寫的上一篇文章中已經解釋過了,這里不再贅述,有需要的小伙伴可以翻閱一下。
??prototype是個指向對象的指針,可以簡單一點來記憶,它揍是原型對象!咱們依舊用 Person 這個類來簡單描述一下:
??Person() 是構造函數,也就是設計圖、模板,照著這個模板,我們可以捏出來一個有名字、有年齡、活生生的“人”,這個過程就叫做實例化,被捏出來的這個“人”就是 Person 類的一個實例,而 在原型(prototype 上,我們則可以放置一些公有的屬性和方法,比如捏出來(實例化)的人,名字雖然都不一樣,但可以定義一個共享的方法,讓每個人都能說出自己的名字。
??但是 prototype 只有構造函數,或者說只有函數才有,普通的實例不存在原型這么個玩意,那它是腫么訪問到原型上的方法的呢?這里就需要介紹到對象擁有的一種屬性: __proto__(注意:前后都是兩個下劃線)
??__proto__同樣是個指針,而且指向的是原型對象(prototype),另外,由于原型對象也是個對象,所以他也有__proto__屬性( Person.prototype.__proto__
??根據上面所說的總結一下:
??1、每個構造函數( Person() )都有一個原型對象( prototype
??2、原型對象都包含一個指向構造函數的指針( constructor )
??3、實例都包含一個指向原型對象的內部指針( __proto__

??所以創建一個“人(person)”后,通過這一些系列的操作,最終就會使得person.__proto__== Person().prototype
??那如果我們讓原型對象和另一個類型的實例相等,比如我們還有一個類:function Biology(){} //生物類,我們讓Person.prototype = new Biology(),OK,現在 Biology 里擁有的屬性和方法 Person 都可以用了,就好比說人類也是生物,都有新陳代謝,都能發育繁殖,之后我們只需要在 Person() 中再添加一些關于“人”獨有的屬性,這樣就成功定義出來了一個“人”類,同理,一個Animal(動物)類也可以通過這種方式來定義,而不用每次都重寫那些生物共有的屬性和方法,這,就叫做繼承!
??如果在 Person 下還有 Father() Son() Grandson() 等一系列類,他們同樣通過上述的方式一層又一層的繼承下來,實例和原型組合成一根長長的鏈條,我們就將之形象的稱為原型鏈

繼承方式

??好了,說完原型問題,咱們就可以正式開始說明怎樣去寫一個繼承,高程三上詳細記錄了六種繼承方式,分別如下所示:
??1.原型繼承
??2.借用構造函數繼承
??3.組合繼承(最優方式)
??4.原型式繼承
??5.寄生式繼承
??6.寄生組合式繼承

??接下來逐一開始介紹。

1.原型繼承

??原型繼承其實就是上面原型鏈里的那個例子,即:

    function Biology() {    //父類--生物類
      this.kind = "我是生物";
    }
    Biology.prototype.growUp = function () {
      console.log(this.kind+",生物都會長大");
    }
    function Person(name,age) {    //子類--人類
      this.name = name;
      this.age = age;
    }
    Person.prototype = new Biology();    //讓人類 繼承 生物類
    Person.prototype.sayName = function () {   //注意:新定義的方法要放在替換原型的語句之后
      console.log(this.name);
    }
    //實例化對象
    var biology = new Biology();
    biology.growUp();    //我是生物,生物都會長大
    var person = new Person("亞當",99);
    person.growUp();   //我是生物,生物都會長大
    person.sayName();   //亞當

??上述代碼中定義了兩個類,一個是‘生物’類,一個是‘人’類,從結果上顯而易見,實例化的“亞當”可以輕松的調用屬于 Biology 的方法(growUp()),而且“人”類還有屬于自己的屬性(name)和方法(sayName()),實現了我們所要的功能。
??這是繼承最基本的寫法,也是最原始的方法,問題有很多:
??第一:無法確定實例和原型的關系,比如上述的亞當,既是“人”類,也是“生物”類,還是一個物體對象(Object),使用instranceof操作符判斷時,三者都返回true
??第二:使用字面量添加新方法時,會重寫原型鏈,導致繼承無效,如:

Person.prototype = {
    sayName : function(){
        //balabala...
    }
}

??這樣寫完以后,person就無法調用BiologygrowUp 方法了。
??第三:如果超類存在引用類型的屬性(如數組等),所有的實例在訪問這個引用類型的屬性時都指向同一塊內存地址,一個做出操作,剩下的都會跟著變化。
??第四:創建子類型的實例時,不能向超類中傳遞參數。
??有鑒于此,實踐中一般很少單獨使用原型鏈實現繼承。

2.借用構造函數(經典繼承)

??基本方法是在子類中調用超類的構造函數,需要借用call()apply()方法,如下所示:

function Biology(kind) {    //父類--生物類
    this.kind = kind;
}
function Person(name,age) {    //子類--人類
    Biology.call(this,"我是人類");
    this.name = name;
    this.age = age;
}

??使用call()apply()方法,可以在借用超類構造函數時傳遞參數(kind),并且在子類中還可以定義新的屬性(name、age)。注意,傳遞的參數只有一個的話,使用call(),若是多個參數,則用apply()方法。
??借用構造函數實現繼承的方法,其問題與用構造函數定義對象相同,即方法全放在了構造函數中,子類實例化時產生了多個同樣的方法,復用性太差,而且超類型原型中定義的方法,對子類不可見,因此,這種方式也很少單獨使用。

3.組合繼承(偽經典繼承)

??顧名思義,就是將原型鏈和構造函數兩種方法組合在一起,取長補短的一種繼承模式。

    function Biology(kind) {    //父類--生物類
      this.kind = kind;
    }
    Biology.prototype.growUp = function () {
      console.log(this.kind+",生物都會長大");
    }
    function Person(name,age) {    //子類--人類
      Biology.call(this,"我是人類");    //繼承屬性
      this.name = name;
      this.age = age;
    }
    Person.prototype = new Biology();    //讓人類 繼承 生物類
    Person.prototype.constructor = Person;  
    Person.prototype.sayName = function () {
      console.log(this.name);
    }

??這種方法是JS中最常用的集成模式,原理是將每個實例獨有的屬性放在構造函數中,而將共享的方法放在原型鏈中,這樣每個實例既有各自獨有的空間,又有公共共享的空間。
??這一方法與組合使用構造函數模式和原型模式的創建對象方法類似,上一篇創建對象文章中有詳細講解,這里不再重復。而且組合繼承同樣存在不足之處,這里暫且按下,待會解釋。

4.原型式繼承

??在沒有必要興師動眾的創建構造函數,而只是想讓一個對象與另一個對象保持類似的時候,可以考慮使用原型式繼承,其原理如下:

function newObject(obj){
    function F(){}
    F.prototype = obj;
    return new F();
}
var child = newObject(person)
child.name = "亞當的孩子"

??創建一個臨時的構造函數,然后將傳入的對象(obj)作為這個構造函數的原型(prototype),最后返回一個臨時類型的實例(new F())。
??ES5中已經將原型式函數規范化,即新增的Object.create()方法,在傳入一個參數的情況下,與上述的newObject()方法相同,如var child = Object.create(person),也可以通過傳入第二個可選的參數,自定義傳入一個對象,覆蓋原型對象上的同名屬性,如下所示。

var child = Object.create(person,{
    name:{
        value:"亞當的孩子"
    }
})

??這種方式的缺陷依舊在于引用類型的屬性,所有繼承了超類的實例,都可以隨意改變引用類型的內容,而且會互相影響,共享內存空間。

5.寄生式繼承

??這種方式可以創建一個僅用于封裝繼承過程的函數,在內部可以讓對象做出某些增強,這種方式與原型式繼承密切相關,如下代碼所示:

function createAnother(obj){
    var clone = Object.create(obj);
    clone.saySpecial = function(){
        alert("我變禿了,也變強了");
    }
    return clone;
}
var child = createAnother(person);
child.saySpecial();    //我變禿了,也變強了

??在主要考慮對象,而不是自定義類型和構造函數的情況下,寄生式繼承也是一種有用的模式,而且Object.create();函數并不是必須的,任何能返回新對象的函數都適用于這種模式。它可以說是原型式繼承的一種拓展,但依舊沒有類的概念,無法做到函數復用,與構造函數模式類似。

6.寄生組合式繼承

??上面說到,組合繼承是JS最常用的繼承模式,但它也有不足之處,那就是無論什么情況下,都會調用兩次超類型的構造函數,一次是在創建子類型原型的時候(new),另一次是在子類型構造函數內部(call()),最終子類型會包含超類型對象的全部實例屬性,我們要在調用子類型構造函數的時候重寫這些屬性。如下所示:

    function Biology(kind) {    //父類--生物類
      this.kind = kind;
    }
    Biology.prototype.growUp = function () {
      console.log(this.kind+",生物都會長大");
    }
    function Person(name,age) {    //子類--人類
      Biology.call(this,"我是人類");    //第二次調用Biology()
      this.name = name;
      this.age = age;
    }
    Person.prototype = new Biology();    //第一次調用Biology()
    Person.prototype.constructor = Person;  
    Person.prototype.sayName = function () {
      console.log(this.name);
    }

??調用了兩次,就是創建了兩次同名的屬性,只不過后面那次把前面的覆蓋掉了而已。基于這種情況,便有了更進一步的寄生組合式繼承,總結起來就是一句話:
??通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。
??簡單一點的原理就是:不必每次都調用父類型的設計圖(構造函數),拷個副本下來就可以嘍!所以,先用寄生式繼承來繼承超類型的原型,然后再將結果指定給子類型的原型。如下代碼所示:

function inheritPrototype(Super,Sub){
    var superProtoClone = Object.Create(Super.prototype)
    superProtoClone.constructor = Sub
    Sub.prototype = Super
}

??第一步:通過寄生式繼承,創建一個超類原型的副本。
??第二步:彌補因重寫原型而失去的默認的constructor屬性。
??第三部:將新創建的對象(副本)賦給子類型的原型。
??這樣,我們就可以通過調用這個函數,省去Person.prototype = new Biology();這一步,后續代碼如下所示:

inheritPrototype(Person,Child)
Person.prototype.sayName = function () {
    console.log(this.name);
}

??這樣做便實現了只調用一次Person的構造函數,避免在子類的原型上創建了多余的屬性,而且原型鏈還能保持不變,可以正常使用instranceof()方法,這種方式是引用類型最理想的繼承方式,但是...過程過于繁瑣,如果可以的話,還是組合繼承來的快一點。

總結

??以上便是JS中關于繼承的內容,時間倉促,描述可能不太細致,若發現文中任何問題,歡迎指正、探討!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容