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

前言

當(dāng)面試官問你:你了解js哪些繼承方式?es6的class繼承是如何實(shí)現(xiàn)的?你心中有很清晰的答案嗎?如果沒有的話,可以通過閱讀本文,幫助你更深刻地理解js的所有繼承方式。

js繼承總共分成5種,包括構(gòu)造函數(shù)式繼承、原型鏈?zhǔn)嚼^承、組合式繼承、寄生式繼承和寄生組合式繼承。

構(gòu)造函數(shù)式繼承

首先來看第一種,構(gòu)造函數(shù)式繼承,顧名思義,也就是利用函數(shù)去實(shí)現(xiàn)繼承;
假設(shè)我們現(xiàn)在有一個(gè)父類函數(shù):

// 父類構(gòu)造函數(shù)
function Parent(color) {
    this.color = color;
    this.print = function() {
        console.log(this.color);
    }
}

現(xiàn)在要編寫一個(gè)子類函數(shù)來繼承這個(gè)父類,如下:

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

上面代碼可以看到,子類Son是通過Parent.call的方式去調(diào)用父類構(gòu)造函數(shù),然后把this對象傳進(jìn)去,執(zhí)行父類構(gòu)造函數(shù)之后,子類Son就擁有了父類定義的color和print方法。調(diào)用一下該方法,輸出如下:

// 測試
var son1 = new Son('red');
son1.print(); // red
?
var son2 = new Son('blue');
son2.print(); // blue

可以看到son1和son2都正常繼承了父類的print方法和各自傳進(jìn)去的color屬性;

以上就是構(gòu)造函數(shù)式繼承的實(shí)現(xiàn)了,這是最原始的js實(shí)現(xiàn)繼承的方式;

但是當(dāng)我們深入想一下會(huì)發(fā)現(xiàn),這種根本就不是傳統(tǒng)意義上的繼承!

因?yàn)槊恳粋€(gè)Son子類調(diào)用父類生成的對象,都是各自獨(dú)立的,也就是說,如果父類希望有一個(gè)公共的屬性是所有子類實(shí)例共享的話,是沒辦法實(shí)現(xiàn)的。什么意思呢,來看下面的代碼:

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()); // [ '黃色', '紅色' ]

我們現(xiàn)在有一個(gè)基類Flower,它有一個(gè)屬性colors,現(xiàn)在我們把某一個(gè)實(shí)例的colors值改一下:

r1.colors.push('紫色');
?
console.log(r1.print()); // [ '黃色', '紅色', '紫色' ]
console.log(r2.print()); // [ '黃色', '紅色' ]

結(jié)果如上,顯然,改變的只有r1的值,因?yàn)橥ㄟ^構(gòu)造函數(shù)創(chuàng)造出來的實(shí)例對象中,所有的屬性和方法都是實(shí)例內(nèi)部獨(dú)立的,并不會(huì)跟其他實(shí)例共享。

總結(jié)一下構(gòu)造函數(shù)的優(yōu)缺點(diǎn):

  • 優(yōu)點(diǎn):所有的基本屬性獨(dú)立,不會(huì)被其他實(shí)例所影響;
  • 缺點(diǎn):所有希望共享的方法和屬性也獨(dú)立了,沒有辦法通過修改父類某一處來達(dá)到所有子實(shí)例同時(shí)更新的效果;同時(shí),每次創(chuàng)建子類都會(huì)調(diào)用父類構(gòu)造函數(shù)一次,所以每個(gè)子實(shí)例都拷貝了一份父類函數(shù)的內(nèi)容,如果父類很大的話會(huì)影響性能;

原型鏈繼承

下面我們來看第二種繼承方式,原型鏈?zhǔn)嚼^承;
同樣先來看下例子:

function Parent() {
    this.color = 'red';
    this.print = function() {
        console.log(this.color);
    }
}
function Son() {
}

我們有一個(gè)父類和一個(gè)空的子類;

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

接著我們把子函數(shù)的原型屬性賦值給了父函數(shù)的實(shí)例;

var son1 = new Son();
son1.print(); // red

最后新建子類實(shí)例,調(diào)用父類的方法,成功拿到父類的color和print屬性方法;
我們重點(diǎn)來分析一下下面兩行代碼:

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

這段代碼中,子函數(shù)的原型賦給了父函數(shù)的實(shí)例,我們知道prototype是函數(shù)中的一個(gè)屬性,js的一個(gè)特性就是:如果一個(gè)對象某個(gè)屬性找不到,會(huì)沿著它的原型往上去尋找,直到原型鏈的最后才會(huì)停止尋找。關(guān)于原型更多基礎(chǔ)的知識,可以參考一下其他文章,或許以后我也會(huì)出一期專門講解原型和原型鏈的文章。

回到代碼,我們看到最后實(shí)例son成功調(diào)用了Print方法,輸出了color屬性,這是因?yàn)閟on從函數(shù)Son的prototype屬性上面去找到的,也就是從new Parent這個(gè)對象里面找到的;

這種方式也不是真正的繼承,因?yàn)樗械淖訉?shí)例的屬性和方法,都在父類同一個(gè)實(shí)例上了,所以一旦某一個(gè)子實(shí)例修改了其中的方法,其他所有的子實(shí)例都會(huì)被影響,來看下代碼:

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子類選擇了原型鏈繼承,所以,子實(shí)例r1修改了colors之后,r2實(shí)例的colors也被改動(dòng)了,這就是原型鏈繼承不好的地方。

來總結(jié)下原型鏈繼承的優(yōu)缺點(diǎn):

  • 優(yōu)點(diǎn):很好的實(shí)現(xiàn)了方法的共享;
  • 缺點(diǎn):正是因?yàn)槭裁炊脊蚕砹?,所以?dǎo)致一切的屬性都是共享的,只要某一個(gè)實(shí)例進(jìn)行修改,那么所有的屬性都會(huì)變化;

組合式繼承

這里來介紹第三種繼承方式,組合式繼承;

這種繼承方式很好理解,既然構(gòu)造函數(shù)式繼承和原型鏈繼承都有各自的優(yōu)缺點(diǎn),那么我們把它們各自的優(yōu)點(diǎn)整合起來,不就完美了嗎?

image.png

組合式繼承做的就是這個(gè)事情~來看一段代碼例子:

?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來調(diào)用父類構(gòu)造函數(shù),同時(shí)又將Son.prototype賦給了父類實(shí)例;為什么要這樣做呢?為什么這樣就能解決上面兩種繼承的問題呢?

我們接著分析一下,使用Parent.call調(diào)用了父類構(gòu)造函數(shù)之后,那么,以后所有通過new Son創(chuàng)建出來的實(shí)例,就單獨(dú)拷貝了一份父類構(gòu)造函數(shù)里面定義的屬性和方法,這是前面構(gòu)造函數(shù)繼承所提到的一樣的原理;

然后,再把子類原型prototype賦值給父類的實(shí)例,這樣,所有子類的實(shí)例對象就可以共享父類原型上定義的所有屬性和方法。這也不難理解,因?yàn)樽訉?shí)例會(huì)沿著原型鏈去找到父類函數(shù)的原型。

因此,只要我們定義父類函數(shù)的時(shí)候,將私有屬性和方法放在構(gòu)造函數(shù)里面,將共享屬性和方法放在原型上,就能讓子類使用了。

以上就是組合式繼承,它很好的融合了構(gòu)造函數(shù)繼承和原型鏈繼承,發(fā)揮兩者的優(yōu)勢之處,因此,它算是真正意義上的繼承方式。

寄生式繼承

既然上面的組合式繼承都已經(jīng)這么完美了,為什么還需要其他的繼承方式呢?

我們細(xì)想一下,Son.prototype = new Parent();這行代碼,它有什么問題沒有?

顯然,每次我們實(shí)例化子類的時(shí)候,都需要調(diào)用一次父類構(gòu)造函數(shù),那么,如果父類構(gòu)造函數(shù)是一個(gè)很大很長的函數(shù),那么每次實(shí)例化子類就會(huì)執(zhí)行很長時(shí)間。

實(shí)際上我們并不需要重新執(zhí)行父類函數(shù),我們只是想要繼承父類的原型。

寄生式繼承就是在做這個(gè)事情,它是基于原型鏈?zhǔn)嚼^承的改良版:

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

寄生式繼承本質(zhì)上還是原型鏈繼承,Object.create(obj);方法意思是以obj為原型構(gòu)造對象,所以寄生式繼承不需要構(gòu)造函數(shù),但是同樣有著原型鏈繼承的優(yōu)缺點(diǎn),也就是它把所有的屬性和方法都共享了。

寄生組合式繼承

接下來到我們最后一個(gè)繼承方式,也就是目前業(yè)界最為完美的繼承解決方案:寄生組合式繼承。

沒錯(cuò),它就是es6的class語法實(shí)現(xiàn)原理

但是如果你理解了組合式繼承,那么理解這個(gè)方式也很簡單,只要記住,它出現(xiàn)的主要目的,是為了解決組合式繼承中每次都需要new Parent導(dǎo)致的執(zhí)行多一次父類構(gòu)造函數(shù)的缺點(diǎn)。

下面來看代碼:

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

這段代碼不同之處只有一個(gè),就是把原來的Son.prototype = new Parent();修改為了Son.prototype = Object.create(Parent.prototype);

我們前面講過,Object.create方法是以傳入的對象為原型,創(chuàng)建一個(gè)新對象;創(chuàng)建了這個(gè)新對象之后,又賦值給了Son.prototype,因此Son的原型最終指向的其實(shí)就是父類的原型對象,和new Parent是一樣的效果;

到這里,我們5中js的繼承方式也就講完了;

image.png

如果你對上面的內(nèi)容感到疑問或者不理解的,可以留言和我交流,或者關(guān)注公眾號直接聯(lián)系我~

最后感謝小伙伴的閱讀,如果覺得文章寫的還可以的話,歡迎點(diǎn)個(gè)贊、點(diǎn)個(gè)關(guān)注,我會(huì)持續(xù)輸出優(yōu)質(zhì)的技術(shù)分享文章。?

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

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