都0202年了,你還不知道javascript有幾種繼承方式?

前言
    當面試官問你:你瞭解js哪些繼承方式?es6的class繼承是如何實現的?你心中有很清晰的答案嗎?若是沒有的話,能夠經過閱讀本文,幫助你更深入地理解js的全部繼承方式。
 
    js繼承總共分紅5種,包括構造函數式繼承、原型鏈式繼承、組合式繼承、寄生式繼承和寄生組合式繼承。
 
構造函數式繼承
 
    首先來看第一種,構造函數式繼承,顧名思義,也就是利用函數去實現繼承;
 
    假設咱們如今有一個父類函數:
// 父類構造函數
function Parent(color) {
    this.color = color;
    this.print = function() {
        console.log(this.color);
    }
}

    如今要編寫一個子類函數來繼承這個父類,以下:ios

// 子類構造函數
function Son(color) {
    Parent.call(this, color);
}

    上面代碼能夠看到,子類Son是經過Parent.call的方式去調用父類構造函數,而後把this對象傳進去,執行父類構造函數以後,子類Son就擁有了父類定義的color和print方法。es6

    調用一下該方法,輸出以下:面試

// 測試
var son1 = new Son('red');
son1.print(); // red
var son2 = new Son('blue');
son2.print(); // blue
    能夠看到son1和son2都正常繼承了父類的print方法和各自傳進去的color屬性;
 
    以上就是構造函數式繼承的實現了,這是最原始的js實現繼承的方式;
 
    可是當咱們深刻想一下會發現,這種根本就不是傳統意義上的繼承!
 
 
    由於每個Son子類調用父類生成的對象,都是各自獨立的,也就是說,若是父類但願有一個公共的屬性是全部子類實例共享的話,是沒辦法實現的。什麼意思呢,來看下面的代碼:
function Flower() {
    this.colors = ['黃色', '紅色'];
    this.print = function () {
        console.log(this.colors)
    }
}
​
function Rose() {
    Flower.call(this);
}
​
var r1 = new Rose();
var r2 = new Rose();
​
console.log(r1.print()); // [ '黃色', '紅色' ]
console.log(r2.print()); // [ '黃色', '紅色' ]

    咱們如今有一個基類Flower,它有一個屬性colors,如今咱們把某一個實例的colors值改一下:函數

r1.colors.push('紫色');
​
console.log(r1.print()); // [ '黃色', '紅色', '紫色' ]
console.log(r2.print()); // [ '黃色', '紅色' ]
    結果如上,顯然,改變的只有r1的值,由於經過構造函數創造出來的實例對象中,全部的屬性和方法都是實例內部獨立的,並不會跟其餘實例共享。
 
    總結一下構造函數的優缺點:
  • 優勢:全部的基本屬性獨立,不會被其餘實例所影響;
  • 缺點:全部但願共享的方法和屬性也獨立了,沒有辦法經過修改父類某一處來達到全部子實例同時更新的效果;同時,每次建立子類都會調用父類構造函數一次,因此每一個子實例都拷貝了一份父類函數的內容,若是父類很大的話會影響性能;
原型鏈繼承
 
    下面咱們來看第二種繼承方式,原型鏈式繼承;
 
    一樣先來看下例子:
function Parent() {
    this.color = 'red';
    this.print = function() {
        console.log(this.color);
    }
}
function Son() {
}

    咱們有一個父類和一個空的子類;性能

Son.prototype = new Parent();
Son.prototype.constructor = Son;

    接着咱們子函數的原型屬性賦值給了父函數的實例;測試

var son1 = new Son();
son1.print(); // red
    最後新建子類實例,調用父類的方法,成功拿到父類的color和print屬性方法;
 
    咱們重點來分析一下下面兩行代碼:
Son.prototype = new Parent();
Son.prototype.constructor = Son;
    這段代碼中,子函數的原型賦給了父函數的實例,咱們知道prototype是函數中的一個屬性,js的一個特性就是:若是一個對象某個屬性找不到,會沿着它的原型往上去尋找,直到原型鏈的最後纔會中止尋找。
 
    關於原型更多基礎的知識,能夠參考一下其餘文章,或許之後我也會出一期專門講解原型和原型鏈的文章。
 
    回到代碼,咱們看到最後實例son成功調用了Print方法,輸出了color屬性,這是由於son從函數Son的prototype屬性上面去找到的,也就是從new Parent這個對象裏面找到的;
 
   
    這種方式也不是真正的繼承,由於全部的子實例的屬性和方法,都在父類同一個實例上了,因此一旦某一個子實例修改了其中的方法,其餘全部的子實例都會被影響,來看下代碼: 
function Flower() {
    this.colors = ['黃色', '紅色'];
    this.print = function () {
        console.log(this.colors)
    }
}
​
function Rose() {}
Rose.prototype = new Flower();
Rose.prototype.constructor = Rose;
​
var r1 = new Rose();
var r2 = new Rose();
​
console.log(r1.print()); // [ '黃色', '紅色' ]
console.log(r1.print()); // [ '黃色', '紅色' ]
​
r1.colors.push('紫色');
​
console.log(r1.print()); // [ '黃色', '紅色', '紫色' ]
console.log(r2.print()); // [ '黃色', '紅色', '紫色' ]
    仍是剛纔的例子,此次Rose子類選擇了原型鏈繼承,因此,子實例r1修改了colors以後,r2實例的colors也被改動了,這就是原型鏈繼承很差的地方。
 
    來總結下原型鏈繼承的優缺點:
  • 優勢:很好的實現了方法的共享;
  • 缺點:正是由於什麼都共享了,因此致使一切的屬性都是共享的,只要某一個實例進行修改,那麼全部的屬性都會變化 
組合式繼承
 
    這裏來介紹第三種繼承方式,組合式繼承;
 
    這種繼承方式很好理解,既然構造函數式繼承和原型鏈繼承都有各自的優缺點,那麼咱們把它們各自的優勢整合起來,不就完美了嗎?
 

 

 

   
 
 
 
 
 
 
 
組合式繼承作的就是這個事情~來看一段代碼例子:
function Parent(color) {
    this.color = color;
}
Parent.prototype.print = function() {
    console.log(this.color);
}
function Son(color) {
    Parent.call(this, color);
}
Son.prototype = new Parent();
Son.prototype.constructor = Son;
​
var son1 = new Son('red');
son1.print(); // red
var son2 = new Son('blue');
son2.print(); // blue
    上面代碼中,在Son子類中,使用了Parent.call來調用父類構造函數,同時又將Son.prototype賦給了父類實例;爲何要這樣作呢?爲何這樣就能解決上面兩種繼承的問題呢?
 
 
    咱們接着分析一下,使用Parent.call調用了父類構造函數以後,那麼,之後全部經過new Son建立出來的實例,就單獨拷貝了一份父類構造函數裏面定義的屬性和方法這是前面構造函數繼承所提到的同樣的原理;
 
    而後,再把子類原型prototype賦值給父類的實例,這樣,全部子類的實例對象就能夠共享父類原型上定義的全部屬性和方法。這也不難理解,由於子實例會沿着原型鏈去找到父類函數的原型。
 
    所以,只要咱們定義父類函數的時候,將私有屬性和方法放在構造函數裏面,將共享屬性和方法放在原型上,就能讓子類使用了。
 
    以上就是組合式繼承,它很好的融合了構造函數繼承和原型鏈繼承,發揮二者的優點之處,所以,它算是真正意義上的繼承方式。
 
寄生式繼承
 
    既然上面的組合式繼承都已經這麼完美了,爲何還須要其餘的繼承方式呢?
 
 
    咱們細想一下,Son.prototype = new Parent();這行代碼,它有什麼問題沒有?
 
    顯然,每次咱們實例化子類的時候,都須要調用一次父類構造函數,那麼,若是父類構造函數是一個很大很長的函數,那麼每次實例化子類就會執行很長時間。
 
    實際上咱們並不須要從新執行父類函數,咱們只是想要繼承父類的原型。
 
    寄生式繼承就是在作這個事情,它是基於原型鏈式繼承的改良版:
 
var obj = {
    color: 'red',
    print: function() {
        console.log(this.color);
    }
};
​
var son1 = Object.create(obj);
son1.print(); // red
var son2 = Object.create(obj);
son2.print(); // red

    寄生式繼承本質上仍是原型鏈繼承,Object.create(obj);方法意思是以obj爲原型構造對象,因此寄生式繼承不須要構造函數,可是一樣有着原型鏈繼承的優缺點,也就是它把全部的屬性和方法都共享了。this

寄生組合式繼承
 
    接下來到咱們最後一個繼承方式,也就是目前業界最爲完美的繼承解決方案:寄生組合式繼承。
 
    沒錯,它就是es6的class語法實現原理。
 
    可是若是你理解了組合式繼承,那麼理解這個方式也很簡單,只要記住,它出現的主要目的,是爲了解決組合式繼承中每次都須要new Parent致使的執行多一次父類構造函數的缺點。
 
    下面來看代碼:
function Parent(color) {
    this.color = color;
}
Parent.prototype.print = function() {
    console.log(this.color);
}
function Son(color) {
    Parent.call(this, color);
}
Son.prototype = Object.create(Parent.prototype);
Son.prototype.constructor = Son;
​
var son1 = new Son('red');
son1.print(); // red
var son2 = new Son('blue');
son2.print(); // blue
    這段代碼不一樣之處只有一個,就是把原來的Son.prototype = new Parent();修改成了Son.prototype = Object.create(Parent.prototype);
 
    咱們前面講過,Object.create方法是以傳入的對象爲原型,建立一個新對象;建立了這個新對象以後,又賦值給了Son.prototype,所以Son的原型最終指向的其實就是父類的原型對象,和new Parent是同樣的效果;
 
 
    到這裏,咱們5中js的繼承方式也就講完了;

 

 

 

 
 
 
 
 
 
 
 
 
    若是你對上面的內容感到疑問或者不理解的,能夠留言和我交流,或者關注公衆號直接聯繫我~
 
    最後感謝小夥伴的閱讀,若是以爲文章寫的還能夠的話,歡迎點個贊、點個關注,我會持續輸出優質的技術分享文章。​
 
相關文章
相關標籤/搜索