詳解js中的繼承(二)

前言

趁週末結束以前趕忙先把坑填上。上回咱們說到了原型鏈,而且留下了幾個思考題,先把答案公佈一下。數組

  1. 在最後一個例子裏,console.log(b1.constructor),結果是什麼?
    答案:function A,由於b1自己沒有constructor屬性,會沿着原型鏈向上找到B prototype對象,而後再往上找到A prototype對象,此時找到了constructor屬性,也就是指向函數對象A,可參見上文最後一張圖片app

  2. B.prototype = new A();B.prototype.sayB = function(){ console.log("from B") }這兩句的執行順序能不能交換?
    答案:不能,由於咱們說過了,第一句是把改寫B函數對象的prototype指向的原型對象,若是咱們交換了順序,是在原先的B的原型對象上綁定了方法,而後再把指針指向新的原型對象,那新的原型對象上天然就沒有綁定sayB方法,接下來的b1.sayB()就會報函數未定義錯誤,函數

  3. 在最後一個例子裏,A看似已是原型鏈的最頂層,那A還能再往上嗎?
    答案,能夠,由於其實全部的引用類型都默認繼承了了Object,也就是說,完整的原型鏈應該是A prototype[Prototype]屬性指向Object prototype。如圖:學習

完整的原型鏈
順便補充一下,Object prototype上的原生方法,包括咱們經常使用的hasOwnProperty()isPropertyOf()等。this

接着談繼承

在上一篇咱們講解了原型鏈的原理,建議沒有理解清楚的讀者朋友先理解以前的知識點,避免難點疊加spa

原型鏈的缺陷

  1. 引用類型的值在原型鏈傳遞中存在的問題
    咱們知道js中有值類型和引用類型,其中引用類型包括Object.Array等,引用類型的值有一個特色:在賦值的時候,賦給變量的是它在內存中的地址。換句話說,被賦值完的變量至關於一個指針,這會有什麼問題呢?看例子:prototype

    function A() {
            this.name = "a" 
            this.color = ['red','green'];         
        }
        function B(){
    
        }
         //讓B的原型對象指向A的一個實例
         B.prototype = new A();
         
         //生成兩個個B的實例
         var b1 = new B();
         var b2 = new B();
         //觀察color屬性
         console.log(b1.name)//a
         console.log(b2.name)//a
         console.log(b1.color)//[red,green]
         console.log(b2.color)//[red,green]
         //改變b1的name和color屬性
         b1.name = 'b'
         b1.color.push('black')
         
         //從新觀察color屬性
         console.log(b1)//b
         console.log(b2)//a
         console.log(b2.name)
         console.log(b1.color)//["red", "green", "black"]
         console.log(b2.color)//["red", "green", "black"]

    發現問題了嗎?咱們修改了b1的color和name屬性,可是b2name屬性不變,color屬性發生了改變。爲了搞清楚這裏問題,請嘗試回答個人問題(想不出來的話,能夠本身經過在控制檯打印出來驗證):指針

    1. b1b2有本身的color屬性嗎?
      答案:沒有,只是B prototype上有color屬性,由於它是A的一個實例,b1b2實際上是經過[Proto]屬性訪問B prototype上的color屬性(指針),從而訪問和操做color數組的;code

    2. b1b2有本身的name屬性嗎?
      答案:一開始都沒有,當執行了b1.name = 'b'時,至關於b1有了本身的name屬性,而b2依然沒有name屬性。orm

    因此以上問題的緣由來源就是咱們前面說的:引用類型的值在賦值的時候,賦給變量的是它在內存中的地址。(若是關於值類型和引用類型有沒掌握的同窗能夠先去看看或者私下問我,這裏默認這個是已經瞭解的。)
    因此在原型鏈中若是A(其實就是繼承中的父類型)含有引用類型的值,那麼子類型的實例共享這個引用類型得值,也就是上面的color數組,這就是原型鏈的第一個缺陷。

  2. 第二個缺陷是:在建立子類型的實例(如b1,b2)時,沒法向父類型的構造函數中傳遞參數。好比在上面的例子中,若是Aname屬性是要傳遞參數的而不是寫死的,那麼咱們在實例化b1b2的時候根本無法傳參

借用構造函數繼承

爲了解決引用類型值帶來的問題,咱們會採用借用構造函數繼承的方式,又名*僞造對象或者經典繼承,核心思路是:咱們在子類型的構造函數中調用父類型的構造函數,這裏要用到一個方法call()或者apply()函數,關於這個函數,我這裏簡單介紹一下,能夠簡單的理解功能就是,容許一個對象調用另外一個對象的方法。具體的做用若是你們以爲須要能夠在評論區回覆,我會後面單獨寫一下這兩個函數。在這裏就不展開了。具體實現以下:

function A() {
            this.name = "a" 
            this.color = ['red','green'];         
        }
        function B(){
          //「借用」|就體如今這裏,子類型B借用了父類型A的構造函數,從而在這裏實現了繼承
          A.call(this);
        }
       
         
         //生成兩個個B的實例
         var b1 = new B();
         var b2 = new B();
         //觀察color屬性
         console.log(b1.name)//a
         console.log(b2.name)//a
         console.log(b1.color)//['red','green']
         console.log(b2.color)//['red','green']
         //改變b1的name和color屬性
         b1.name = 'b'
         b1.color.push('black')
         
         //從新觀察屬性
         console.log(b1.name)//b
         console.log(b2.name)//a
         console.log(b1.color)//['red','green','black']
         console.log(b2.color)//["red", "green"]

在這裏咱們沒有采用原型鏈,而是利用call()方法來實現在子類型的構造函數中借用父類型的構造函數,完成了繼承,這樣繼承的結果就是:b1,b2都分別擁有本身的namecolor屬性(能夠直接console.log(b1)查看對象的屬性),也就是b1b2徹底獨立的。這就解決了以前的第一個問題,並且傳遞參數的問題其實也能夠解決,再稍微改一下A函數:

//這裏name改爲傳遞參數的
        function A(name) {
            this.name = name 
            this.color = ['red','green'];         
        }
        function B(name){
          //在這裏咱們接受一個參數,而且經過call方法傳遞到A的構造函數中
          A.call(this,name);
        }
       
         
         //生成兩個個B的實例
         var b1 = new B('Mike');
         var b2 = new B('Bob');
         //觀察屬性
         console.log(b1.name)//Mike
         console.log(b2.name)//Bob
         console.log(b1.color)//['red','green']
         console.log(b2.color)//['red','green']

其實上面就能夠直接寫成這樣,可是爲了讓你們更容易理解,故意分開,隔離變量(你們看我這麼用心真的不考慮點個贊嗎?),順便再解釋一下A.call(this,name);就是讓this對象(這裏是指B)調用構造函數A,同時傳入一個參數name

能夠看到,借用構造函數繼承不會有原型鏈繼承的問題,那爲何不都借用採用構造函數繼承的方法呢?緣由在於:這種繼承方式,全部的屬性和方法都要在構造函數中定義,好比咱們這裏也要綁定以前的sayA()方法並繼承,就只能寫在A的構造函數裏面,而寫在A prototype的的方法,無法經過這種方式繼承,而把全部的屬性和方法都要在構造函數中定義的話,就不能對函數方法進行復用.

組合繼承

學習了原型鏈的繼承和借用構造函數的繼承後,咱們能夠發現,這兩種方法的優缺點恰好互補:

  • 原型鏈繼承能夠把方法定義在原型上,從而複用方法

  • 借用構造函數繼承法能夠解決引用類型值的繼承問題和傳遞參數問題

所以,就天然而然的想到,結合這兩種方法,因而就有了下面的組合繼承,也叫僞經典繼承,(前面的借用構造函數是經典繼承,能夠聯繫起來),具體實現以下:

function A(name) {
            this.name = name 
            this.color = ['red','green'];     
        }
        A.prototype.sayA = function(){
          console.log("form A")
        }
        function B(name,age){
          //借用構造函數繼承
          A.call(this,name);
          this.age = age;
        }

        //原型鏈
        B.prototype = new A();
        B.prototype.sayB = function(){
          console.log("form B")
        }
         
         //生成兩個個B的實例
         var b1 = new B('Mike',12);
         var b2 = new B('Bob',13);
         //觀察color屬性
         console.log(b1)//{name:'Mike'...}
         console.log(b2)//{name:'Bob'...}
         b1.sayA()//from A
         b2.sayB()//from B

這個例子只是對上面的例子稍做修改:

  1. 咱們在A prototype上定義了sayA() ,在B prototype 定義了sayB()

  2. 咱們增長了B.prototype = new A();原型鏈

最終實現的效果就是,b1和b2都有各自的屬性,同時方法都定義在兩個原型對象上,這就達到了咱們的目的:屬性獨立,方法複用,這種繼承的理解相對簡單,由於就是把前兩種繼承方式簡單的結合一下,原型鏈負責原型對象上的方法,call借用構造函數負責讓子類型擁有各自的屬性。
組合繼承是js中最經常使用的繼承方式

原型式繼承

原型式繼承與以前的繼承方式不太相同,原理上至關於對對象進行一次淺複製,淺複製簡單的說就是:把父對像的屬性,所有拷貝給子對象。可是咱們前面說到,因爲引用類型值的賦值特色,因此屬性若是是引用類型的值,拷貝過去的也僅僅是個指針,拷貝完後父子對象的指針是指向同一個引用類型的(關於深複製和淺複製若是須要細講的一樣能夠在評論區留言。)原型式繼承目前能夠經過Object.create()方式來實現,(這個函數的原理我不想在這裏提,由於我但願讀者在看完這裏內容之後本身去查閱一下這個內容)本文只講實現方式:
Object.create()接收兩個參數:

  • 第一個參數是做爲新對象的原型的對象

  • 第二個參數是定義爲新對象增長額外屬性的對象(這個是可選屬性)

  • 若是沒有傳遞第二個參數的話,就至關於直接運行object()方法(這個方法若是不懂直接百度就好)
    上面的說法可能有點拗口,換句話說:

好比說咱們如今要建立一個新對象B,那麼要先傳入第一個參數對象A,這個A將被做爲B prototype;而後能夠再傳入一個參數對象CC對象中能夠定義咱們須要的一些額外的屬性。來看例子

var A  = {
        name:'A',
        color:['red','green']
    }

    //使用Object.create方法先複製一個對象
    var B = Object.create(A);
    B.name = 'B';
    B.color.push('black');

    //使用Object.create方法再複製一個對象
    var C = Object.create(A);
    C.name = 'C';
    B.color.push('blue');
    console.log(A.name)//A
    console.log(B.name)//B
    console.log(C.name)//C
    console.log(A.color)//["red", "green", "black", "blue"]

在這個例子中,咱們只傳入第一個參數,因此BC都是對A淺複製的結果,因爲name是值類型的,color是引用類型的,因此ABC的name值獨立,color屬性指向同一個對象。接下來舉個傳遞兩個參數的例子:

var A  = {
        name:'A',
        color:['red','green'],
        sayA:function(){
            console.log('from A');
        }
    };

    //使用Object.create方法先複製一個對象
    var B = Object.create(A,{
        name:{
          value:'B'
        }
    });
    console.log(B)//Object{name:'B'}
    B.sayA()//'from A'

這個例子就很清楚的代表了這個函數的做用了,傳入的A對象被當作B的原型,因此生成B對象沒有sayA()方法,卻能夠調用該方法(相似於經過原型鏈),同時咱們在第二個參數中修改了B本身的name,因此就實現了這種原型式繼承。原型式繼承的好處是:若是咱們只是簡單的想保持一個對象和另外一個對象相似,沒必要大費周章寫一堆代碼,直接調用就能實現

寄生式繼承

寄生式繼承和原型繼承聯繫緊密,思路相似於工廠模式,即建立一個只負責封裝繼承過程的函數,在函數中根據須要加強對象,最後返回對象

function createA(name){
    //建立新對象
    var obj = Object(name);
    //加強功能
     obj.sayO = function(){
         console.log("from O")
     };
    //返回對象
    return obj;
     
}
var A = {
    name:'A',
    color:['red','green','blue']
};
//實現繼承
var  B = createA(A);
console.log(B)//Object {name: "A", color: Array[3]}
B.sayO();//from O

繼承的結果是B擁有A的全部屬性和方法,並且具備本身的sayO()方法,效果和原型式繼承很類似,讀者能夠比較一下寄生式繼承和原型式繼承的類似和區別。

寄生組合式繼承

終於寫到最後一個繼承了,咱們在以前講了5種繼承方式,分別是原型鏈借用構造函數繼承組合繼承原型式繼承寄生式繼承,其中,前三種聯繫比較緊密,後面兩種也比較緊密,而咱們要講的最後一種,是和組合繼承還有寄生式繼承有關係的。(看名字就知道了嘛)

友情提示:若是看到這裏有點累的讀者能夠先休息一下,由於雖然已經分了一二兩篇,本文的篇幅仍是稍長(我都打了兩個多小時了),並且若是先把以前的理解清楚,比較容易理解最後一種繼承。

組合繼承仍有缺陷

咱們在以前說過,最經常使用的繼承方式就是組合繼承,可是看似完美的組合繼承依然有缺點:子類型會兩次調用父類型的構造函數,一次是在子類型的構造函數裏,另外一次是在實現原型鏈的步驟,來看以前的代碼:

function A(name) {
            this.name = name 
            this.color = ['red','green'];     
        }
        A.prototype.sayA = function(){
          console.log("form A")
        }
        function B(name,age){
         //第二次調用了A
          A.call(this,name);
          this.age = age;
        }

        //第一次調用了A
        B.prototype = new A();
        B.prototype.sayB = function(){
          console.log("form B")
        }
         

         var b1 = new B('Mike',12);
         var b2 = new B('Bob',13);
          console.log(B.prototype)//A {name: undefined, color: Array[2]}

在第一次調用的時候,生成了B.prototype對象,它具備namecolor屬性,由於它是A的一個實例;第二次調用的時候,就是實例化b1b2的時候,這時候b1b2也具備了namecolor屬性,咱們以前說過,原型鏈的意義是:當對象自己不存在某個屬性或方法的時候,能夠沿着原型鏈向上查找,若是對象自身已經有某種屬性或者方法,就訪問自身的,可是咱們如今發現,經過組合繼承,只要是A裏面原有的屬性,B prototype對象必定會有,b1b2確定也會有,這樣就形成了一種浪費:B prototyope上的屬性其實咱們根本用不上,爲了解決這個問題,咱們採用寄生組合式繼承。
寄生組合式繼承的核心思路是其實就是換一種方式實現 B.prototype = new A();從而避免兩次調用父類型的構造函數,官方定義是:使用寄生式繼承來繼承父類型的原型,而後將結果指定給子類型的原型,。`這句話不容易理解,來看例子:

//咱們一直默認A是父類型,B是子類型
function inheritPrototype(B,A){
    //複製一個A的原型對象
    var pro  = Object(A.prototype);
    //改寫這個原型對象的constructor指針指向B
    pro.constructor = B;
    //改寫B的prototype指針指向這個原型對象
    B.prototype = pro;
}

這個函數很簡短,只有三行,函數內部發生的事情是:咱們複製一個A的原型對象,而後把這個原型對象替換掉B的原型對象。爲何說這樣就代替了 B.prototype = new A();,不妨思考一下,咱們最初爲何要把B的prototype屬性指向A的一個實例?無非就是想獲得A的prototype的一個複製品,而後實現原型鏈。而如今咱們這樣的作法,一樣達到了咱們的母的目的,並且,此時B的原型對象上不會再有A的屬性了,由於它不是A的實例。所以,只要把將上面的 B.prototype = new A();,替換成inheritPrototype(B,A),就完成了寄生組合式繼承。

寄生組合式繼承保持了組合繼承的優勢,又避開了組合繼承會有無用屬性的缺陷,被認爲是最理想的繼承方式。

小結

終於寫完了!! 明天還得起早去上班,下一次更新可能會放在這一週的週末。關於這一篇內容,建議的閱讀方式是先讀前三種繼承方式,再看後兩種繼承,都理解的差很少了,就能夠看最後一種繼承方式了。中間注意消化和休息。最後再提一下吧:若是喜歡本文,請大方的點一下右上角的推薦和收藏(反正大家仍是喜歡只收藏不推薦),雖說寫這個一方面是爲了本身鞏固知識,可是爲了讓讀者更容易理解,我儘可能都是採用拆解的方式來說,並且穿插了新知識的時候都會給出解釋,並非直接搬運書本知識過來,那樣毫無心義。這麼作仍是但願寫的文章可以更有價值,讓更多人可以獲得幫助!以上內容屬於我的看法,若是有不一樣意見,歡迎指出和探討。請尊重做者的版權,轉載請註明出處,如做商用,請與做者聯繫,感謝!

相關文章
相關標籤/搜索