javascript
一文梳理JavaScript中常见的七大继承方案
闡述JavaScript中常見(jiàn)的七大繼承方案
- 📖序言
- 📔文章內(nèi)容搶先看
- 📝一、基礎(chǔ)知識(shí)預(yù)備
- 1. 繼承的定義
- 2. 繼承的方式
- 📚二、6大常見(jiàn)繼承方式
- 1. 原型鏈繼承 💡
- (1)構(gòu)造函數(shù)、原型和實(shí)例的關(guān)系
- (2)基本思想
- (3)實(shí)現(xiàn)原型鏈繼承
- (4)圖例闡述
- (5)破壞原型鏈
- (6)優(yōu)缺點(diǎn)
- 2. 盜用構(gòu)造函數(shù)繼承💡
- (1)基本思想
- (2)使用方式
- (3)實(shí)現(xiàn)原型鏈繼承
- (4)優(yōu)缺點(diǎn)
- 3. 組合繼承💡
- (1)基本思想
- (2)實(shí)現(xiàn)組合繼承
- (3)圖例
- (4)優(yōu)缺點(diǎn)
- 4. 原型式繼承💡
- (1)基本思想
- (2)實(shí)現(xiàn)原型式繼承
- (3)優(yōu)缺點(diǎn)
- 5. 寄生式繼承💡
- (1)基本思想
- (2)實(shí)現(xiàn)寄生式繼承
- (3)優(yōu)缺點(diǎn)
- 6. 寄生式組合繼承💡
- (1)基本思想
- (2)實(shí)現(xiàn)寄生式組合繼承
- (3)圖例
- (4)優(yōu)缺點(diǎn)
- 🗞?三、Class的繼承
- 1. 基本概念
- 2. Object.getPrototypeOf()
- 3. super關(guān)鍵字
- (1)作為函數(shù)使用
- (2)作為對(duì)象使用
- 1)在普通方法中
- 2)在靜態(tài)方法中
- 4. 類的prototype屬性和 __ proto __ 屬性
- (1)class的繼承鏈
- (2)特殊情況繼承
- (3)實(shí)例的 __ proto __ 屬性
- 📑四、結(jié)束語(yǔ)
- 🐣彩蛋 One More Thing
- (:參考資料
- (:番外篇
📖序言
在前端的面試中,繼承是一道很常考的題目,面試官總會(huì)變著法來(lái)問(wèn)你。比如說(shuō),你知道哪幾種繼承方式?這幾種繼承方式有什么區(qū)別?這幾種繼承方式的優(yōu)缺點(diǎn)是啥?又各有什么特點(diǎn)呢?
一招致命,就像周一第一次遇到這道題的時(shí)候,只記得有那么幾種繼承方式,但是我也說(shuō)不上個(gè)所以然,知識(shí)還是太浮動(dòng)于表面了。
因此,寫下這篇文章,來(lái)總結(jié)各種繼承方式的知識(shí)點(diǎn)。一起來(lái)學(xué)習(xí)叭~?
📔文章內(nèi)容搶先看
在真正進(jìn)入本文的講解之前,我們先用一張思維導(dǎo)圖來(lái)了解本文所涉及到的內(nèi)容。詳情見(jiàn)下圖👇
下面開(kāi)始進(jìn)入本文的講解~
📝一、基礎(chǔ)知識(shí)預(yù)備
1. 繼承的定義
引用 Javascript 高級(jí)語(yǔ)言程序設(shè)計(jì)中的一句話:
繼承是面向?qū)ο缶幊?(即 object-oriented language ,下面簡(jiǎn)稱OO語(yǔ)言) 中討論的最多的一個(gè)話題。很多 OO語(yǔ)言 都支持兩種繼承:接口繼承和實(shí)現(xiàn)繼承。前者只能繼承方法簽名,而后者可以繼承實(shí)際的方法。
然后,接口繼承在 ECMAScript 中是不太可能存在的,原因在于函數(shù)沒(méi)有簽名。
因此呢,實(shí)現(xiàn)繼承是 ECMAScript 唯一支持的繼承方式,且這主要通過(guò)原型鏈來(lái)實(shí)現(xiàn)。
2. 繼承的方式
了解完定義,我們來(lái)看一下,繼承有多少種方法。詳情見(jiàn)下圖👇
📚二、6大常見(jiàn)繼承方式
1. 原型鏈繼承 💡
(1)構(gòu)造函數(shù)、原型和實(shí)例的關(guān)系
對(duì)于構(gòu)造函數(shù)、原型和實(shí)例三者之間,有以下關(guān)系:每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象,原型上有一個(gè)屬性指回構(gòu)造函數(shù),而實(shí)例又有一個(gè)內(nèi)部指針指向原型。
(2)基本思想
原型鏈繼承的基本思想是通過(guò)原型來(lái)繼承多個(gè)引用類型的屬性和方法,其核心在于將父類的實(shí)例作為子類的原型。
(3)實(shí)現(xiàn)原型鏈繼承
上面的內(nèi)容可能看起來(lái)有點(diǎn)抽象,我們先用一個(gè)例子來(lái)體驗(yàn)一下原型鏈繼承。具體代碼如下:
// 父類函數(shù) function SuperType() {// 父類定義屬性this.property = true; }// 父類定義方法 SuperType.prototype.getSuperValue = function() {return this.property; }// 子類函數(shù) function SubType() {this.subproperty = false; }/*** 關(guān)鍵點(diǎn):* 通過(guò)創(chuàng)建父類SuperType的實(shí)例,* 并將該實(shí)例賦值給子類SubType.prototype*/ SubType.prototype = new SuperType(); // 子類定義新方法 SubType.prototype.getSubValue = function() {return this.subproperty; }// 創(chuàng)建一個(gè)子類的實(shí)例 let instance = new SubType(); // 子類調(diào)用父類的方法 console.log(instance.getSuperValue()); // true(4)圖例闡述
依據(jù)以上代碼,我們來(lái)用一張圖表示這段代碼的繼承關(guān)系。如下圖所示:
大家定位到圖片的最右上角區(qū)域,同時(shí)我們也將由右上到左下進(jìn)行分析。
首先,我們會(huì)創(chuàng)建構(gòu)造函數(shù) SuperType 的實(shí)例,并將其作為子類的原型,也就是 SuperType.prototype 。之后,實(shí)例創(chuàng)建完了,該實(shí)例上有一個(gè)內(nèi)容指針指向父類的原型 SuperType.prototype 。完成之后,來(lái)到了第三步。該原型上有一個(gè)屬性將指回構(gòu)造函數(shù) SuperType 。第四步,對(duì)于構(gòu)造函數(shù)來(lái)說(shuō),每個(gè)構(gòu)造函數(shù)又有它自己的原型,所以它又會(huì)指回 SuperType.prototype 。
依據(jù)上面這段描述,大家再看下(1)和(2)的關(guān)系和基本思想,是不是就清晰許多了呢。
同樣地,下面的⑤~⑧步也是和上面一樣的步驟,大家可以自行再理解對(duì)應(yīng),這里不再細(xì)述。
(5)破壞原型鏈
還有一個(gè)要理解的重點(diǎn)是,以對(duì)象字面量的方式去創(chuàng)建原型方法回破壞之前的原型鏈,因?yàn)檫@相當(dāng)于重寫了原型鏈。如下例子所示:
function SuperType() {this.property = true; }SuperType.prototype.getSuperValue = function() {return this.property; };function SubType() {this.subproperty = false; }// 繼承SuperType SubType.prototpe = new SuperType(); // 通過(guò)對(duì)象字面量添加新方法,這將導(dǎo)致上一行無(wú)效 SubType.prototype = {getSubValue() {return this.subproperty;},someOtherMoethod(){return false;} }let instance = new SubType(); console.log(instance.getSuperValue()); // TypeError: instance.getSuperValue is not a function在上面這段代碼中,子類 SubType 的原型在被賦值為 SuperType 的實(shí)例后,又被一個(gè)對(duì)象字面量覆蓋了。而覆蓋后的原型其實(shí)已經(jīng)變成了一個(gè) Object 的實(shí)例,而不再是 SuperType 的實(shí)例,因此在賦值為對(duì)象字面量之后,原型鏈就被斷掉了。這個(gè)時(shí)候 SubType 和 SuperType 也就不再有任何關(guān)系。
(6)優(yōu)缺點(diǎn)
1)優(yōu)點(diǎn):
- 父類的方法可以復(fù)用。
2)缺點(diǎn):
- 父類的所有引用類型數(shù)據(jù)將會(huì)被所有子類共享,也就是說(shuō),一旦有一個(gè)子類的引用類型數(shù)據(jù)更改了,那么其他子類也會(huì)受到影響。
- 子類型實(shí)例不能給父類型構(gòu)造函數(shù)傳參。原因在于,一旦傳參的話,由于第一個(gè)問(wèn)題的因素,會(huì)導(dǎo)致當(dāng)傳相同的參數(shù)時(shí)會(huì)覆蓋之前的。
2. 盜用構(gòu)造函數(shù)繼承💡
(1)基本思想
對(duì)于以上原型鏈繼承存在的兩個(gè)問(wèn)題,所以導(dǎo)致原型鏈基本不會(huì)被單獨(dú)使用。因此,為了解決原型包含引用值導(dǎo)致的問(wèn)題,我們引入一種新的繼承方式,為 “盜用構(gòu)造函數(shù)” 。這種技術(shù)也被稱為對(duì)象偽裝或者經(jīng)典繼承。
其基本思路為: 使用父類的構(gòu)造函數(shù)來(lái)增強(qiáng)子類實(shí)例,等同于賦值父類的實(shí)例給子類(不使用原型)。
(2)使用方式
函數(shù)是在特定上下文中執(zhí)行代碼的簡(jiǎn)單對(duì)象,因此,可以使用 apply() 和 call() 方法,以新創(chuàng)建的對(duì)象為上下文來(lái)執(zhí)行構(gòu)造函數(shù)。
(3)實(shí)現(xiàn)原型鏈繼承
我們用一個(gè)例子來(lái)實(shí)現(xiàn)原型鏈繼承。具體代碼如下:
function SuperType(name) {this.name = name;this.colors = ["red", "blue", "green"]; }function SubType(name, age) {// 繼承SuperType并傳參,這里是核心SuperType.call(this, 'monday');// 實(shí)例屬性this.age = age; }let instance1 = new SubType("monday", 18); instance1.colors.push("gray"); console.log(instance1.name); // monday console.log(instance1.age); // 18 console.log(instance1.colors); // [ 'red', 'blue', 'green', 'gray' ]let instance2 = new SubType(); console.log(instance2.colors); // [ 'red', 'blue', 'green' ]大家可以看到,在上面的這個(gè)例子中, 通過(guò)使用 call() 方法,借用了 SuperType 構(gòu)造函數(shù),這樣,在創(chuàng)建 SubType 的實(shí)例時(shí),都會(huì)將 SuperType 中的屬性復(fù)制一份出來(lái)。
同時(shí),相對(duì)于原型鏈來(lái)說(shuō),盜用構(gòu)造函數(shù)的一個(gè)優(yōu)點(diǎn)就是可以在子類構(gòu)造函數(shù)中向父類構(gòu)造函數(shù)傳遞參數(shù)。大家可以看到上面代碼,我們?cè)谑褂?SuperType.call() 時(shí),就可以將參數(shù)直接傳遞給父類 SuperType 。
(4)優(yōu)缺點(diǎn)
1)優(yōu)點(diǎn):
- 子類的構(gòu)造函數(shù)可以向父類的構(gòu)造函數(shù)傳遞參數(shù)。
2)缺點(diǎn):
- 只能繼承父類的實(shí)例屬性和方法,不能繼承父類的原型屬性和方法。
- 無(wú)法實(shí)現(xiàn)復(fù)用,每新創(chuàng)建一個(gè)子類,都會(huì)產(chǎn)生父類實(shí)例函數(shù)的副本,非常影響性能。
3. 組合繼承💡
(1)基本思想
由于原型鏈和盜用構(gòu)造函數(shù)繼承的方式都存在一定的缺陷,所以它們倆基本上不能單獨(dú)使用。為此呢,我們引入了一種新的繼承方式,組合繼承。組合繼承也叫做偽經(jīng)典繼承,它綜合了原型鏈和盜用構(gòu)造函數(shù),將兩者的優(yōu)點(diǎn)結(jié)合了起來(lái)。
其實(shí)現(xiàn)的基本思路為:使用原型鏈來(lái)繼承原型上的屬性和方法,然后呢,通過(guò)盜用構(gòu)造函數(shù)來(lái)繼承實(shí)例上的屬性。
這樣,通過(guò)在原型上定義方法實(shí)現(xiàn)了函數(shù)的復(fù)用,又能保證每個(gè)實(shí)例都有它自己的屬性。
(2)實(shí)現(xiàn)組合繼承
下面我們用一個(gè)例子來(lái)實(shí)現(xiàn)組合繼承。具體代碼如下:
function SuperType(name) {this.name = name;this.colors = ['red', 'blue', 'green']; }SuperType.prototype.sayName = function() {console.log(this.name); }function SubType(name, age) {// 繼承屬性→借用構(gòu)造函數(shù)繼承實(shí)例上的屬性SuperType.call(this, name);this.age = age; }// 繼承方法→通過(guò)原型的方式繼承原型的屬性和方法 SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){console.log(this.age); }let instance1 = new SubType('Monday', 18); instance1.colors.push('gray'); console.log(instance1.colors); // [ 'red', 'blue', 'green', 'gray' ] instance1.sayName(); // Monday instance1.sayAge(); // 18let instance2 = new SubType('Tuesday', 24); console.log(instance2.colors); // [ 'red', 'blue', 'green' ] instance2.sayName(); // Tuesday instance2.sayAge(); // 24大家可以看到,在以上的代碼中,通過(guò)盜用構(gòu)造函數(shù),繼承了父類 SuperType 實(shí)例上的屬性。同時(shí),通過(guò)原型的方式,也成功繼承了父類 SuperType 原型上的屬性和方法。
(3)圖例
我們來(lái)用一張圖展示一下上述的結(jié)果。具體如下:
(4)優(yōu)缺點(diǎn)
1)優(yōu)點(diǎn):
- 避開(kāi)原型鏈和盜用構(gòu)造函數(shù)繼承的缺陷,實(shí)現(xiàn)對(duì)實(shí)例和原型的繼承。
- 組合繼承是 javascript 常用的一種繼承方式,且保留了 instanceof 操作符和 isPrototypeOf() 方法識(shí)別合成對(duì)象的能力。
2)缺點(diǎn):
- 父類中的實(shí)例屬性和方法,既存在于子類的實(shí)例中,又存在于子類的原型中,這將會(huì)占用更多的內(nèi)存。
- 所以,在使用子類創(chuàng)建實(shí)例對(duì)象時(shí),其原型中會(huì)存在兩份相同的屬性和方法。
4. 原型式繼承💡
(1)基本思想
原型式繼承實(shí)現(xiàn)的基本思路是,將某個(gè)對(duì)象直接賦值給構(gòu)造函數(shù)的原型。如下代碼所示:
function object(obj) {function F(){}F.prototype = obj;return new F(); }以上這段代碼是 Douglas Crockford 在2006年寫的一篇文章中所談及。這篇文章介紹了一種不涉及嚴(yán)格意義上構(gòu)造函數(shù)的繼承方法,且他的出發(fā)點(diǎn)是即使不自定義類型,也可以通過(guò)原型來(lái)實(shí)現(xiàn)對(duì)象之間的信息共享。
在上述代碼中, object() 對(duì)傳入其中的對(duì)象執(zhí)行了一次,并將 F 的原型直接指向傳入的對(duì)象。
(2)實(shí)現(xiàn)原型式繼承
下面我們用一個(gè)例子來(lái)實(shí)現(xiàn)原型式繼承。具體代碼如下:
function object(obj){function F(){}F.prototype = obj;return new F();}let person = {name: 'Monday',friends: ['January', 'February', 'March'] };let otherPerson = object(person); otherPerson.name = 'Friday'; otherPerson.friends.push('April');let anotherPerson = object(person); anotherPerson.name = 'Sunday'; anotherPerson.friends.push('May');console.log(person.friends); // [ 'January', 'February', 'March', 'April', 'May' ]大家可以看到,最終所有的 friend 都拷貝到了 person 對(duì)象上。但是這種方法其實(shí)用的比較少,有點(diǎn)類似于原型鏈繼承的模式,所以一般也不會(huì)單獨(dú)使用。
(3)優(yōu)缺點(diǎn)
1)優(yōu)點(diǎn):
- 適用于不需要單獨(dú)創(chuàng)建構(gòu)造函數(shù),但仍然需要在對(duì)象間共享信息的場(chǎng)合。
2)缺點(diǎn):
- 當(dāng)繼承多個(gè)實(shí)例的引用類型數(shù)據(jù)時(shí),指向是相同的,所以存在篡改的可能。
- 無(wú)法傳遞參數(shù)。
- ES5 中已經(jīng)存在 Object.create() 的方法,能夠代替上面的 object 方法。
5. 寄生式繼承💡
(1)基本思想
寄生式繼承的基本思想是,在上面原型式繼承的基礎(chǔ)上,以某種方式來(lái)增強(qiáng)對(duì)象,并返回這個(gè)對(duì)象。
(2)實(shí)現(xiàn)寄生式繼承
下面我們用一個(gè)例子來(lái)實(shí)現(xiàn)寄生式繼承,具體代碼如下:
// object函數(shù) function object(obj){function F(){}F.prototype = obj;return new F(); }// 函數(shù)的主要作用是為構(gòu)造函數(shù)新增屬性和方法,以增強(qiáng)函數(shù) function createAnother(original) {// 通過(guò)調(diào)用函數(shù)來(lái)創(chuàng)建一個(gè)新對(duì)象let clone = object(original);// 以某種方式增強(qiáng)這個(gè)對(duì)象clone.sayHello = function() {console.log('hello');}// 返回對(duì)象return clone; }let person = {name: 'Monday',friends: ['January', 'February', 'March'] };let anotherPerson = createAnother(person); anotherPerson.sayHello(); // hello大家可以看到,通過(guò)創(chuàng)建一個(gè)新的構(gòu)造函數(shù) createAnother ,來(lái)增強(qiáng)對(duì)象的內(nèi)容。并在之后對(duì)這個(gè)對(duì)象進(jìn)行返回,供給我們使用。
(3)優(yōu)缺點(diǎn)
1)優(yōu)點(diǎn):
- 適合只關(guān)注對(duì)象本身,而不在乎數(shù)據(jù)類型和構(gòu)造函數(shù)的場(chǎng)景。
2)缺點(diǎn):
- 給對(duì)象添加函數(shù)會(huì)導(dǎo)致函數(shù)難以重用,與上面第2中的盜用構(gòu)造函數(shù)繼承類似。
- 當(dāng)繼承多個(gè)實(shí)例的引用類型數(shù)據(jù)時(shí),指向是相同的,所以存在篡改的可能。
- 無(wú)法傳遞參數(shù)。
6. 寄生式組合繼承💡
(1)基本思想
寄生式組合繼承的基本思想是,結(jié)合盜用構(gòu)造函數(shù)和寄生式模式來(lái)實(shí)現(xiàn)繼承。
(2)實(shí)現(xiàn)寄生式組合繼承
我們用一個(gè)例子,來(lái)展示寄生式組合繼承。具體代碼如下:
function inheritPrototype(subType, superType) {// 創(chuàng)建對(duì)象let prototype = Object.create(superType.prototype);// 增強(qiáng)對(duì)象prototype.constructor = subType;// 指定對(duì)象subType.prototype = prototype; }// 父類對(duì)實(shí)例屬性和原型屬性進(jìn)行初始化 function SuperType(name) {this.name = name;this.friends = ['January', 'February', 'March']; } SuperType.prototype.sayName = function() {console.log(this.name); }// 借用構(gòu)造函數(shù)對(duì)子類實(shí)例屬性進(jìn)行傳參(支持傳參和避免篡改) function SubType(name, age) {SuperType.call(this, name);this.age = age; }// 運(yùn)用寄生式繼承的特點(diǎn),將父類的原型指向子類 inheritPrototype(SubType, SuperType);// 新增子類的原型屬性 SubType.prototype.sayAge = function() {console.log(this.age); }let instance1 = new SubType('Ida', 18); let instance2 = new SubType('Hunter', 23);instance1.friends.push('April'); console.log(instance1.friends); // [ 'January', 'February', 'March', 'April' ] instance1.friends.push('May'); console.log(instance2.friends); // [ 'January', 'February', 'March' ]大家可以看到,對(duì)于寄生式組合繼承來(lái)說(shuō),它借用了構(gòu)造函數(shù)來(lái)對(duì)子類的實(shí)例屬性進(jìn)行傳參,同時(shí)通過(guò)運(yùn)用寄生式繼承的特點(diǎn),使用 inheritPrototype 構(gòu)造函數(shù)來(lái)將父類的原型指向子類,這樣,子類就繼承了父類的原型 。
同時(shí),子類還可以在自己的原型上新增自己想要的原型屬性,達(dá)到繼承自己的原型方法的效果。
(3)圖例
我們?cè)賮?lái)用一張圖展示一下上述的結(jié)果。具體如下:
(4)優(yōu)缺點(diǎn)
1)優(yōu)點(diǎn):
- 寄生式組合繼承可以算是引用類型繼承的最佳方式。
- 幾乎避免了上述所有繼承方式中所存在的缺陷,也是執(zhí)行效率最高且應(yīng)用面最廣的。
2)缺點(diǎn):
- 實(shí)現(xiàn)的過(guò)程相對(duì)較繁瑣,要捋好父子之間的關(guān)系,避免跳入原型的漩渦中。這其實(shí)也不算啥缺點(diǎn),因?yàn)樗档?#xff01;
🗞?三、Class的繼承
1. 基本概念
在上述中我們講到了各種類型的繼承方式,但這似乎都逃不開(kāi)原型鏈的閉圈中。到了 2015 年,ES6 的出現(xiàn)解決了這個(gè)問(wèn)題。 ES6 的 class 可以通過(guò) extends 關(guān)鍵字來(lái)實(shí)現(xiàn)繼承,這間接地,比 ES5 通過(guò)修改原型鏈實(shí)現(xiàn)繼承的方式更加清晰和方便了許多。
我們用一個(gè)例子來(lái)先看一下 class 是如何實(shí)現(xiàn)繼承的。具體代碼如下:
class Point{constructor(x, y) {this.x = x;this.y = y;} }class ColorPoint extends Point {constructor(x, y, color) {// 調(diào)用父類的 constructor(x, y)// 只有super方法才能返回父類的實(shí)例super(x, y); this.color = color;}toString() {return this.color + ' ' + super.toString() {// 調(diào)用父類的toString()}} }大家可以看到,通過(guò) extends 關(guān)鍵字,實(shí)現(xiàn)了對(duì)父類的屬性和方法進(jìn)行繼承,這似乎看起來(lái)就比上面的寄生式組合繼承要方便的多了。
接下來(lái),我們繼續(xù)看其他用法。
2. Object.getPrototypeOf()
在 ES6 中, Object.getPrototypeof 方法可以用來(lái)從子類上獲取父類。比如:
Object.getPrototypeof(ColorPoint) === Point // true所以,可以使用這一方法來(lái)判斷一個(gè)類是否繼承了另外一個(gè)類。
3. super關(guān)鍵字
上面我們有看到 super 這個(gè)關(guān)鍵字,它主要是用來(lái)返回父類的實(shí)例。那在實(shí)際的應(yīng)用中, super 可以當(dāng)作函數(shù)使用,也可以當(dāng)作對(duì)象使用。接下來(lái)我們來(lái)談?wù)勥@兩種情況。
(1)作為函數(shù)使用
第一種情況,當(dāng) super 作為函數(shù)調(diào)用時(shí),代表父類的構(gòu)造函數(shù)。 ES6 要求,子類的構(gòu)造函數(shù)必須執(zhí)行一次 super 函數(shù)。如下例子:
class A {}class B extends A {constructor() {super();} }上述代碼中的 super() 代表調(diào)用父類 A 的構(gòu)造函數(shù),但是返回的是子類 B 的實(shí)例,即 super 內(nèi)部的 this 指的是 B 。因此,這里的 super() 相當(dāng)于 A.prototype.constructor.call(this) 。
值得注意的是, 作為函數(shù)時(shí), super() 只能用在子類的構(gòu)造函數(shù)之中,用在其他地方會(huì)報(bào)錯(cuò)。比如:
class A {}class B extends A {m() {super(); // 報(bào)錯(cuò)} }大家可以看到,像上面這種情況, super() 用在 B 類的 m 方法之中就會(huì)造成句法錯(cuò)誤。
(2)作為對(duì)象使用
第二種情況,當(dāng) super 作為對(duì)象時(shí),在普通方法中指向父類的原型對(duì)象,在靜態(tài)方法中指向父類。我們來(lái)一一分析這兩種情況。
1)在普通方法中
先來(lái)看一段代碼,具體如下:
class A {p() {return 2;} }class B extends A {constructor() {super();console.log(super.p()); // 2} }let b = new B();在上面的代碼中,子類 B 中的 super.p() 就是將 super 當(dāng)作一個(gè)對(duì)象來(lái)使用。依據(jù)上面所描述的, super 在普通方法中指向父類的原型對(duì)象,因此,這里的 super.p() 就等于 A.prototype.p() 。所以最終輸出為 2 。
繼續(xù),由于 super 指向父類的原型對(duì)象,所以,定義在父類實(shí)例上的方法或?qū)傩詴r(shí)無(wú)法通過(guò) super 進(jìn)行調(diào)用的。比如:
class A {constructor() {this.p = 2;} }class B extends A {get m() {return super.p; // 定義在普通方法上,所以super指向父類的原型對(duì)象} }let b = new B(); b.m // undefined在上述代碼中, super 在普通方法中調(diào)用,所以也就意味著 super 指向父類的原型對(duì)象。而 b.m 卻想要直接指向父類 A 實(shí)例的屬性上,自然就是搜索不到它的。
我們可以對(duì)代碼進(jìn)行修改,將屬性定義在父類的原型對(duì)象上,讓 super 可以找到具體的屬性。具體如下:
class A {} A.prototype.x = 2;class B extends A {constructor() {super();console.log(super.x); // 2} }let b = new B();大家可以看到,現(xiàn)在屬性 x 是定義在 A.prototype 上面的,所以 super.x 就可以順利的取到它的值了。
2)在靜態(tài)方法中
上面我們談到了 super 作為對(duì)象時(shí)在普通方法中的調(diào)用,現(xiàn)在我們來(lái)看一下在靜態(tài)方法中的的調(diào)用是什么樣的。
首先,我們談到的一個(gè)點(diǎn)就是, super 在靜態(tài)方法中被調(diào)用時(shí),指向的是其父類。我們來(lái)看一段代碼:
class Parent {static myMethod(msg) {console.log('static', msg);}myMethod(msg) {console.log('instance', msg);} }class Child extends Parent {static myMethod(msg) {super.myMethod(msg);}myMethods(msg) {super.myMethods(msg);} }Child.myMethod(1); // static 1 let child = new Child(); child.myMethod(2); // instance 2大家可以看到,當(dāng)直接使用 Child.myMethod(1) 來(lái)進(jìn)行調(diào)用時(shí),表明直接調(diào)用靜態(tài)方法。而當(dāng) super 在靜態(tài)方法中被調(diào)用時(shí),指向的就是其父類中的靜態(tài)方法,所以打印 static 1 。
繼續(xù),下面的 child ,通過(guò) new 的方式實(shí)例化了一個(gè)對(duì)象,之后對(duì)實(shí)例進(jìn)行調(diào)用。所以,此時(shí)調(diào)用的是普通方法,所以最終打印出 instance 1 。
在使用 super 的時(shí)候,必須要注意的一個(gè)點(diǎn)是,必須顯式地指定是作為函數(shù)還是作為對(duì)象使用,否則就會(huì)出現(xiàn)報(bào)錯(cuò)的情況。比如:
class A {} class B extends A {constructor() {super();console.log(super); // 報(bào)錯(cuò)} }大家可以看到,如果像上面這樣引用的話,根本無(wú)法看出是作為函數(shù)還是作為對(duì)象來(lái)引用,此時(shí) js 引擎解析代碼的時(shí)候,就會(huì)報(bào)錯(cuò)。
那么我們就得通過(guò)清晰的表明 super 的數(shù)據(jù)類型,來(lái)判斷我們的結(jié)果。比如:
class A {} class B extends A {constructor() {super();// object.valueOf() 表示返回對(duì)象的原始值console.log(super.valueOf() instanceof B); // true} }上述代碼中,我們通過(guò) object.valueOf() 來(lái)指明 super 就是一個(gè)對(duì)象,之后 js 引擎識(shí)別到了,最后也就成功打印出來(lái)了。
4. 類的prototype屬性和 __ proto __ 屬性
(1)class的繼承鏈
在大多數(shù)瀏覽器的 ES5 實(shí)現(xiàn)之中,每一個(gè)對(duì)象都有 __proto__ 屬性,指向?qū)?yīng)的構(gòu)造函數(shù)的 prototype 屬性。
而 class 作為構(gòu)造函數(shù)的語(yǔ)法糖,同時(shí)有 prototype 屬性和 __proto__ 屬性,因此同時(shí)存在兩條繼承鏈。分別是:
- 子類的 __proto__ 屬性表示構(gòu)造函數(shù)的繼承,總是指向父類。
- 子類 prototype 屬性的 __proto__ 屬性表示方法的繼承,總是指向父類的 prototype 屬性。
我們用一段代碼來(lái)演示一下:
class A {}class B extends A {}B.__proto__ === A // true B.prototype.__proto__ === A.prototype // true在上面的代碼中,子類 B 的 __proto__ 屬性總是指向父類 A ,且子類 B 的 prototype 屬性的 __proto__ 屬性指向父類 A 的 prototype 屬性。
這兩條原型鏈可以理解為:
- 當(dāng)作為一個(gè)對(duì)象時(shí),子類 B 的原型( __proto__ 屬性 )是父類 A ;
- 當(dāng)作為一個(gè)構(gòu)造函數(shù)時(shí),子類 B 的原型( prototype 屬性 )是父類的實(shí)例。
(2)特殊情況繼承
下面討論三種特殊的繼承情況。具體如下:
第一種情況: 子類繼承 Object 類。先來(lái)看一段代碼:
class A extends Object {}A.__proto__ === Object // true A.prototype.__proto__ === Object.prototype // true在這種情況下, A 其實(shí)就是構(gòu)造函數(shù) Object 的復(fù)制, A 的實(shí)例就是 Object 的實(shí)例。
第二種情況: 不存在任何繼承。先來(lái)看一段代碼:
class A {}A.__proto__ === Function.prototype // true B.prototype.__proto__ === Object.prototype // true在以上這種情況下, A 作為一個(gè)基類,它不存在任何繼承關(guān)系。且 class 是構(gòu)造函數(shù)的語(yǔ)法糖,所以這個(gè)時(shí)候可以說(shuō) A 就是一個(gè)普通函數(shù)。所以, A 直接繼承 Function.prototype 。
值得注意的是, A 調(diào)用后返回的是一個(gè)空對(duì)象(即 Object 實(shí)例),所以 A.prototype.__proto__ 指向構(gòu)造函數(shù) Object 的 prototype 屬性。
第三種情況: 子類繼承 null 。先來(lái)看一段代碼:
class A extends null {}A.__proto__ === Function.prototype // true A.prototype.__proto__ === undefined // true以上這種情況與第二種情況非常相似。 A 也是一個(gè)普通函數(shù),所以直接繼承 Function.prototype 。
值得注意的是, A 調(diào)用后返回的對(duì)象不繼承任何方法,所以它的 __proto__ 指向 Function.prototype ,即實(shí)際上是執(zhí)行了下面這段代碼:
class C extends null {constructor() {return Object.create(null);} }(3)實(shí)例的 __ proto __ 屬性
對(duì)于子類來(lái)說(shuō),其實(shí)例的 __proto__ 屬性的 __proto__ 屬性總是指向父類實(shí)例的 __proto__ 屬性。也就是說(shuō),子類的原型的原型是父類的原型。
我們來(lái)看一段代碼,具體如下:
let p1 = new Point(2, 3); let p2 = new ColorPoint(2, 3, 'green');p2.__proto__ === p1.__proto__ // false p2.__proto__.__proto__ === p1.__proto__ // true在上面的代碼中, 子類 ColorPoint 繼承了 Point ,所以導(dǎo)致了前者原型的原型是后者的原型。
因此,我們可以通過(guò)子類實(shí)例的 __proto__.__proto__ 屬性來(lái)修改父類實(shí)例的行為。具體如下:
p2.__proto__.__proto__.printName = function() {console.log('Monday'); };p1.printName(); // 'Monday'大家可以看到,通過(guò)在 ColorPoint 的實(shí)例 p2 上向 Point 類中添加方法,從而影響到了 Point 實(shí)例 p1 。
📑四、結(jié)束語(yǔ)
上文我們講到了6大繼承方式以及 class 的繼承,在現(xiàn)如今的開(kāi)發(fā)場(chǎng)景中,基本上也是寄生式組合繼承和 class 的繼承用的比較多。相信通過(guò)上文的了解,大家對(duì) javascript 的繼承又有了一個(gè)新的認(rèn)識(shí)。
到這里,關(guān)于 js 的繼承講解就結(jié)束啦!希望對(duì)大家有幫助!
如文章有誤或有不理解的地方,歡迎小伙伴們?cè)u(píng)論區(qū)留言~💬
🐣彩蛋 One More Thing
(:參考資料
書籍👉ES6書籍《ES6標(biāo)準(zhǔn)入門》
書籍👉紅寶書《JavaScript高級(jí)程序設(shè)計(jì)》第四版
同夢(mèng)奇緣👉《javascript高級(jí)程序設(shè)計(jì)》筆記:繼承
(:番外篇
- 關(guān)注公眾號(hào)星期一研究室,第一時(shí)間關(guān)注優(yōu)質(zhì)文章,更多精選專欄待你解鎖~
- 如果這篇文章對(duì)你有用,記得留個(gè)腳印jio再走哦~
- 以上就是本文的全部?jī)?nèi)容!我們下期見(jiàn)!👋👋👋
總結(jié)
以上是生活随笔為你收集整理的一文梳理JavaScript中常见的七大继承方案的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 魅族21相机样张遭吉利副总曝光 网友:你
- 下一篇: 梦回十年前,消息称一加 12 手机将推出