javascript
这一次带你彻底搞懂JS继承
目錄
前言
起步
"new" 究竟發(fā)生了什么?
類式繼承(原型鏈繼承)
構(gòu)造函數(shù)繼承
組合繼承
原型式繼承
寄生式繼承
寄生組合式繼承
總結(jié)
前言
這段時(shí)間復(fù)習(xí)JS從看懂到看開(kāi)(前端面試題整合)_DieHunter1024的博客-CSDN博客時(shí)發(fā)現(xiàn)對(duì)繼承概念又陌生了,平時(shí)大多用的都是extends,對(duì)底層知識(shí)難免會(huì)生疏,于是決定分享這篇文章,重新學(xué)習(xí)一下繼承。
起步
JavaScript和面向類的語(yǔ)言不同,它沒(méi)有類做對(duì)象的抽象模式,它能夠不通過(guò)類直接創(chuàng)建對(duì)象,相比其他的面向?qū)ο笳Z(yǔ)言,JavaScript才能算是真正的面向 " 對(duì)象 " 語(yǔ)言。在面向類的語(yǔ)言中構(gòu)造函數(shù)通常是屬于類的,而JavaScript中(在ES6之前),類是屬于構(gòu)造函數(shù)的,為什么這么說(shuō)?因?yàn)槲覀兪褂玫念悓?shí)際上是用構(gòu)造函數(shù)實(shí)現(xiàn)的。下面進(jìn)入主題讓我們聊聊繼承。
繼承作為面向?qū)ο蟪绦蛟O(shè)計(jì)特征之一,必定有其重要的意義
繼承是指:在已存在的類的基礎(chǔ)上,拓展出新的類。那么存在的類就是父類,或基類,超類;新的類就是子類,或派生類
其重要意義就是使代碼可以復(fù)用,子類中也擁有父類的屬性和方法,從父類一級(jí)一級(jí)往下,屬性和函數(shù)由泛化到細(xì)化
那么js中的繼承又是怎樣的呢?
"new" 究竟發(fā)生了什么?
要了解繼承,得先了解new,我們?cè)趎ode環(huán)境下看看以下案例
JavaScript中的類在es6之前,沒(méi)有class語(yǔ)法糖時(shí),用的是構(gòu)造函數(shù)實(shí)現(xiàn)的,與class不同的是,構(gòu)造函數(shù)既是類,也是函數(shù),既可以使用 "函數(shù)名()" 的方式執(zhí)行,也可以采用?"new 函數(shù)名()" 的方式執(zhí)行,這二者之間的效果卻是截然不同,下面的例子中,使用 "函數(shù)名()" 的方式執(zhí)行打印的是小暗,而另一個(gè)使用new的卻打印了小明(這里我們是在node環(huán)境下執(zhí)行的,function中的this指向的是全局的global,如果是在瀏覽器控制臺(tái)執(zhí)行,就需要把global換成window),由此可以得知 new 實(shí)際上是把構(gòu)造函數(shù)原型(prototype)上的屬性放在了原型鏈(__proto__)上,那么當(dāng)實(shí)例化對(duì)象取值時(shí)就會(huì)在原型鏈上取,而實(shí)例化對(duì)象上的prototype已經(jīng)不見(jiàn)了
global.name = "小暗"; function Person() {console.log(this.name); } Person.prototype = {name: "小明", }; const fnReturn = Person(); // 小暗 const newReturn = new Person(); // 小明 console.log(fnReturn); // undefined console.log(newReturn.name, newReturn.__proto__, newReturn.prototype); // 小明 { name: '小明' } undefined我們可以簡(jiǎn)單理解為 new 實(shí)際上是將構(gòu)造函數(shù)的prototype上的屬性放在了實(shí)例化對(duì)象的__proto__上 ,通過(guò)實(shí)例化對(duì)象 . 屬性名進(jìn)行取值
那么new如何實(shí)現(xiàn)呢?
來(lái)看看下面的代碼
exports.newClass = function () {const _target = new Object(); // 新增一個(gè)容器,用來(lái)裝載構(gòu)造函數(shù)(目標(biāo)類)prototype上的所有屬性const _this = this; //不能直接通過(guò) this() 來(lái)運(yùn)行構(gòu)造函數(shù),所以用一個(gè)變量裝載_target.__proto__ = _this.prototype; // 核心部分:將構(gòu)造函數(shù)prototype上的所有屬性放到新容器中const result = _this.apply(_target, arguments); // 執(zhí)行構(gòu)造函數(shù),相當(dāng)于執(zhí)行class中的constructorreturn result && typeof result === "object" ? result : _target; // 若函數(shù)返回值為引用類型返回當(dāng)前函數(shù)執(zhí)行結(jié)果,否則將新的容器返回,此時(shí)通過(guò) _target[屬性名]就可以訪問(wèn) this.prototype 中的屬性了 };上述代碼將 new 實(shí)現(xiàn)了一下,其中最重要的一步就是將構(gòu)造函數(shù)prototype上的所有屬性放到新容器中,最后獲得的實(shí)例化對(duì)象的__proto__上就有了構(gòu)造函數(shù)原型中所有屬性了,下面我們放在之前的代碼中看看效果
const { newClass } = require("./lib/new"); function Person() {console.log(this.name); } Person.prototype = {name: "小明", }; const newReturn = new Person(); // 小明 const myNew = newClass.call(Person); // 小明 console.log(newReturn.name, newReturn.__proto__, newReturn.prototype); // 小明 { name: '小明' } undefined console.log(myNew.name, myNew.__proto__, myNew.prototype); // 小明 { name: '小明' } undefined說(shuō)了這么多,其實(shí)目的是為了讓大家知道:實(shí)例化一個(gè)構(gòu)造函數(shù),實(shí)際上可以簡(jiǎn)單理解為將類的prototype上的屬性轉(zhuǎn)移到實(shí)例化對(duì)象中,這樣有助于理解后續(xù)的繼承的實(shí)現(xiàn),話不多說(shuō),直接開(kāi)始
類式繼承(原型鏈繼承)
結(jié)合?new?的原理可以知道:?類式繼承實(shí)際上是通過(guò)?new?將?SuperClass.prototype?綁定到?SuperClass.__proto__?上,然后賦值給?SubClass.prototype,當(dāng)實(shí)例化?SubClass?時(shí),SubClass.__proto__?上也會(huì)帶有?SuperClass?及其原型鏈上的屬性,即?SubClass?實(shí)例化對(duì)象上有以下屬性:SuperClass.prototype?上的屬性(實(shí)例化對(duì)象.__proto__.__proto__),SuperClass?構(gòu)造函數(shù)上的屬性(實(shí)例化對(duì)象.__proto__),SubClass?構(gòu)造函數(shù)上的屬性(實(shí)例化對(duì)象)
function classInheritance(SuperClass, SubClass) {SubClass.prototype = new SuperClass(); } function SuperClass(props) {this.state = props;this.info = { color: "red" }; } SuperClass.prototype = {name: "Car", }; classInheritance(SuperClass, SubClass); function SubClass() {this.price = 1000; } const BMW = new SubClass(); const BenZ = new SubClass(); console.log(BMW, BMW.__proto__, BMW.__proto__.__proto__); // { price: 1000 } { state: undefined, info: { color: 'red' } } { name: 'Car' } console.log(BenZ.name, BenZ.info); // Car { color: 'red' } BMW.info.color = "blue"; console.log(BenZ.name, BenZ.info); // Car { color: 'blue' } console.log(BMW instanceof SubClass) // true console.log(BMW instanceof SuperClass) // true優(yōu)點(diǎn):簡(jiǎn)潔方便,子類擁有父類及父類?prototype?上屬性
缺點(diǎn):
- 子類通過(guò)prototype繼承父類,只能父類單向傳遞屬性給子類,無(wú)法向父類傳遞參數(shù)。為什么要向父類傳遞參數(shù)?如果父類中的某屬性對(duì)參數(shù)有依賴關(guān)系,此時(shí)子類繼承父類就需要在?new?SuperClass()?時(shí)傳參
- 當(dāng)父類原型上的引用屬性改變時(shí),所有子類實(shí)例相對(duì)應(yīng)的引用屬性都會(huì)對(duì)應(yīng)改變,即繼承的引用類型屬性都有引用關(guān)系
- 子類只能繼承一個(gè)父類(因?yàn)槔^承方式是直接修改子類的prototype,如果再次修改,會(huì)將其覆蓋)
- 繼承語(yǔ)句前不能修改子類的?prototype?因?yàn)榇祟惱^承會(huì)覆蓋子類原型
構(gòu)造函數(shù)繼承
在?SubClass?構(gòu)造函數(shù)中使用?SuperClass.call?直接運(yùn)行?SuperClass?構(gòu)造函數(shù),然而直接執(zhí)行構(gòu)造函數(shù)和使用?new?實(shí)例化構(gòu)造函數(shù)二者是完全不同的:
-
前者(直接執(zhí)行構(gòu)造函數(shù))在下方代碼中會(huì)將?SuperClass?構(gòu)造函數(shù)里初始化的屬性帶到?SubClass?中,而?SuperClass.prototype?中的?name?屬性并未帶到?SubClass?中;
-
而后者(使用?new?實(shí)例化構(gòu)造函數(shù))則會(huì)將?SuperClass.prototype?中的屬性帶到?SuperClass?實(shí)例化對(duì)象的?__proto__?上
所以其優(yōu)點(diǎn)是:
-
可以在?SuperClass?執(zhí)行時(shí)傳參數(shù)
-
可以繼承多個(gè)父類
-
繼承同一個(gè)父類的子類的屬性之間不會(huì)有引用關(guān)系(因?yàn)楦割悩?gòu)造函數(shù)的執(zhí)行是在每個(gè)子類中call(this)了,從而在父類構(gòu)造函數(shù)執(zhí)行時(shí),this分別代表著每個(gè)子類)
缺點(diǎn)是:父類 prototype 上的屬性無(wú)法繼承,只能繼承父類構(gòu)造函數(shù)的屬性,正是因?yàn)檫@點(diǎn),父類的函數(shù)無(wú)法復(fù)用(指無(wú)法復(fù)用父類?prototype?中的函數(shù),只能通過(guò)父類構(gòu)造函數(shù)將函數(shù)放在子類中)
針對(duì)父類的函數(shù)無(wú)法復(fù)用的理解:
父類 SuperClass?每次在子類?SubClass?中執(zhí)行都會(huì)在每個(gè)子類重新初始化?this.屬性?或?this.函數(shù),這些屬性是屬于每個(gè)子類單獨(dú)的,這樣既增加了性能負(fù)擔(dān)又使父類原型中的公共屬性無(wú)法復(fù)用;
而倘若這些函數(shù)或者屬性在?SuperClass?的?prototype?上,并且子類能繼承父類,則所有子類用公共屬性的都是父類的,此時(shí)就達(dá)到了復(fù)用效果,而類式繼承卻能夠?qū)崿F(xiàn)這個(gè)效果,于是就有了下面的組合繼承
組合繼承
構(gòu)造函數(shù)繼承不能繼承父類原型上的屬性,而類式繼承無(wú)法傳參給父類,組合繼承正好將兩者規(guī)避了
然而組合繼承在實(shí)例化父類和執(zhí)行父類構(gòu)造函數(shù)時(shí)執(zhí)行了兩次?SuperClass?,實(shí)際上類式繼承是為了解決構(gòu)造函數(shù)繼承上的父類的?prototype?無(wú)法被子類繼承的問(wèn)題,看代碼可以得知,new?SuperClass()?確實(shí)會(huì)將父類的?prototype?繼承到子類中,但是也會(huì)將?SuperClass?構(gòu)造函數(shù)中的操作又執(zhí)行一遍(具體可看?console.log(++count)?執(zhí)行了3次),而且類式繼承是將子類的原型直接替換掉,所以無(wú)法繼承多個(gè)父類的問(wèn)題也被延續(xù)下來(lái)了(但是可以在父類上多加一次繼承,使多個(gè)類形成原型鏈關(guān)系,達(dá)到多繼承的目的,即A,B,C三個(gè)類,A要繼承B和C,那么讓A繼承B再繼承C)
function classInheritance(superClass, subClass) {subClass.prototype = new superClass(); } let count = 0; function SuperClass(props) {this.state = props;this.info = { color: "red" };console.log(++count);// 打印 1 2 3 } SuperClass.prototype = {name: "Car", }; classInheritance(SuperClass, SubClass); function SubClass() {SuperClass.call(this, ...arguments);this.price = 1000; } SubClass.prototype.name = "Small Car"; const BMW = new SubClass(true); const BenZ = new SubClass(false);console.log(BMW, BMW.__proto__, BMW.__proto__.__proto__); // { state: true, info: { color: 'red' }, price: 1000 } { state: undefined, info: { color: 'red' }, name: 'Small Car' } { name: 'Car' } console.log(BenZ.name, BenZ.info, BenZ.state);// Small Car { color: 'red' } false BMW.info.color = "blue"; console.log(BenZ.name, BenZ.info);// Small Car { color: 'red' } console.log(BMW instanceof SubClass) // true console.log(BMW instanceof SuperClass) // true優(yōu)點(diǎn):解決類式繼承和構(gòu)造函數(shù)繼承的主要問(wèn)題
缺點(diǎn):父類構(gòu)造函數(shù)執(zhí)行兩遍,性能損耗
原型式繼承
原型式繼承是基于類式繼承的封裝,特點(diǎn)和類式繼承一樣,繼承的引用類型屬性都有引用關(guān)系
原型式繼承的過(guò)渡對(duì)象F實(shí)際上就是類式繼承中的子類構(gòu)造函數(shù),這么做相比類式繼承的特點(diǎn):減少性能開(kāi)銷(子類是空白的構(gòu)造函數(shù),沒(méi)有任何內(nèi)容),對(duì)應(yīng)的,無(wú)法在子類構(gòu)造函數(shù)中初始化屬性
是不是覺(jué)得原型式繼承和?Object.create( )?很像? create 函數(shù)的原理就是生成一個(gè)新對(duì)象,這個(gè)新對(duì)象的 __proto__ 等于傳入的對(duì)象。讓我們回憶一下前面講到的 new 的原理,new 實(shí)際上就是將?prototype 放在實(shí)例化對(duì)象的?__proto__ 上,不難理解,下面代碼中 F.prototype = superClass 和?new?F() 做的就是這一步
function prototypeInheritance(superClass) {function F() {}F.prototype = superClass;return new F(); } function SuperClass(props) {this.state = props;this.info = { color: "red" }; } SuperClass.prototype = {name: "Car", }; const superClass = new SuperClass(true); const BenZ = prototypeInheritance(superClass); const BMW = prototypeInheritance(superClass); BMW.price = 2000;console.log(BMW, BMW.__proto__, BMW.__proto__.__proto__); // { price: 2000 } { state: true, info: { color: 'red' } } { name: 'Car' } console.log(BenZ, BenZ.name, BenZ.info, BenZ.state); // {} Car { color: 'red' } true BMW.info.color = "blue"; console.log(BenZ.name, BenZ.info); // Car { color: 'blue' } console.log(BMW instanceof SuperClass); // true類式繼承是如何轉(zhuǎn)換成原型式繼承?看以下代碼是不是清晰了一點(diǎn),所以原型式繼承也可以寫(xiě)成const?subClass=Object.create(superClass)
function prototypeInheritance(SuperClass) {function SubClass() {}SubClass.prototype = new SuperClass();return new SubClass(); }優(yōu)點(diǎn):無(wú)子類構(gòu)造函數(shù)開(kāi)銷,相當(dāng)于實(shí)現(xiàn)了對(duì)象的淺復(fù)制
缺點(diǎn):?
- 繼承時(shí)無(wú)法向父類傳參
- 和類式繼承一樣,繼承父類的引用類型屬性都有引用關(guān)系
寄生式繼承
寄生式繼承實(shí)際上是在上面的原型式繼承的基礎(chǔ)上做了二次封裝,可以看成工廠模式+原型式繼承,將繼承步驟放在新的函數(shù)中,此時(shí)便可以在子類構(gòu)造函數(shù)上添加子類獨(dú)有的函數(shù)和屬性,由此叫做寄生式繼承,就好像子類獨(dú)有的屬性方法寄生在下面的?parasiticInheritance?函數(shù)中一樣。使用這種繼承在新建子類時(shí),每個(gè)子類中的屬性都不一樣,違背了代碼復(fù)用的效果
function prototypeInheritance(superClass) {function F() {}F.prototype = superClass;return new F(); } function SuperClass(props) {this.state = props;this.info = { color: "red" }; } SuperClass.prototype = {name: "Car", };function parasiticInheritance(superClass) {const subClass = prototypeInheritance(superClass);subClass.type = { electricity: true, gasoline: false };return subClass; }const superClass = new SuperClass(true); const BenZ = parasiticInheritance(superClass); const BMW = parasiticInheritance(superClass); console.log(BenZ.type === BMW.type); // false 說(shuō)明每個(gè)子類的屬性都不一樣 console.log(BenZ, BenZ.__proto__, BenZ.__proto__.__proto__); // { type: { electricity: true, gasoline: false } } { state: true, info: { color: 'red' } } { name: 'Car' } console.log(BMW, BMW.name, BMW.info, BMW.state); // { type: { electricity: true, gasoline: false } } Car { color: 'red' } true BMW.info.color = "blue"; console.log(BMW.name, BMW.info); // Car { color: 'blue' } console.log(BMW instanceof SuperClass); // true console.log(BenZ instanceof SuperClass); // true優(yōu)點(diǎn):
-
無(wú)子類構(gòu)造函數(shù)開(kāi)銷
-
繼承父類所有屬性
-
子類擁有自己的屬性
缺點(diǎn):
-
繼承時(shí)無(wú)法向父類傳參
-
和類式繼承一樣,繼承父類的引用類型屬性都有引用關(guān)系
-
子類公共屬性無(wú)法在原型上定義,導(dǎo)致無(wú)法復(fù)用
針對(duì)代碼無(wú)法復(fù)用缺點(diǎn)的理解:讓我們回憶一下上面的構(gòu)造函數(shù)繼承對(duì)代碼復(fù)用的理解,子類構(gòu)造函數(shù)中直接執(zhí)行父類構(gòu)造函數(shù)并改變?this?指向從而達(dá)到將父類屬性初始化到子類中。而寄生式繼承則是每次生成的子類都是新的構(gòu)造函數(shù)?F?,所以在繼承時(shí)單獨(dú)給?subClass?增加屬性實(shí)際上是操作不同的子類構(gòu)造函數(shù),而如果這個(gè)做法能在子類?prototype?中進(jìn)行,那么子類的函數(shù)及屬性可以復(fù)用。
寄生組合式繼承
實(shí)際上上述繼承方式都是實(shí)現(xiàn)最終繼承方式的猜想和嘗試,在ES6的class語(yǔ)法糖出現(xiàn)之前,寄生組合式繼承是最理想的繼承方式,下面讓我們來(lái)看看
顧名思義寄生組合式繼承就是寄生式繼承和組合式繼承的結(jié)合,個(gè)人認(rèn)為叫它寄生組合式繼承倒不如稱其為原型組合式繼承,因?yàn)樗膶?xiě)法就是原型式繼承+組合式繼承
作為ES6之前最理想的繼承,我們當(dāng)然是要深入分析一下,這么做到底好在哪?
我們按照標(biāo)題寄生組合式繼承實(shí)現(xiàn)一下這種繼承的寫(xiě)法
// 之前寫(xiě)的原型式繼承 function prototypeInheritance(superClass) {function F() {}F.prototype = superClass;return new F(); }function parasiticCombinatorialInheritance(SuperClass, SubClass) {// 核心代碼SubClass.prototype = prototypeInheritance(SuperClass.prototype);SubClass.prototype.superClass = SuperClass; } // 父類 function SuperClass(props) {} // 子類 function SubClass() {this.superClass.call(this, ...arguments); } parasiticCombinatorialInheritance(SuperClass, SubClass);乍一看,這種寫(xiě)法和組合式繼承屬實(shí)有點(diǎn)像,但是有一點(diǎn)不同:
prototypeInheritance 函數(shù)會(huì)生成一個(gè)只包含父類原型上屬性而沒(méi)有執(zhí)行父類構(gòu)造函數(shù)的 “純凈” 的新對(duì)象(即不執(zhí)行父類構(gòu)造函數(shù))。
這句話怎么理解?
讓我們結(jié)合一下 new 的原理,回憶一下類式繼承或組合式繼承是如何實(shí)現(xiàn)的:SubClass.prototype?=?new?SuperClass() 這樣會(huì)導(dǎo)致子類 prototype 中既執(zhí)行了父類構(gòu)造函數(shù),也有父類原型上的屬性。而實(shí)際上我們是暫時(shí)不需要執(zhí)行父類構(gòu)造函數(shù)的,因?yàn)樵诮M合式繼承中還有一步:在子類中執(zhí)行 SuperClass.call(this, ...arguments) ,這一步會(huì)將父類構(gòu)造函數(shù)再執(zhí)行一次,將其二者結(jié)合,于是我們就得到了組合式繼承的升級(jí)版:寄生組合式繼承
我們將prototypeInheritance簡(jiǎn)寫(xiě)成Object.create,得到以下示例
function parasiticCombinatorialInheritance(SuperClass, SubClass) {SubClass.prototype = Object.create(SuperClass.prototype);SubClass.prototype.superClass = SuperClass; }function SuperClass(props) {this.state = props;this.info = { color: "red" }; } SuperClass.prototype = {name: "Car", };function SubClass() {this.superClass.call(this, ...arguments); //調(diào)用一下父類構(gòu)造函數(shù),將父類的屬性放在子類中 }parasiticCombinatorialInheritance(SuperClass, SubClass); SubClass.prototype.name = "small car";//修改prototype值寫(xiě)在繼承后面 const BMW = new SubClass(true); const BenZ = new SubClass(false); console.log(BenZ, BenZ.__proto__, BenZ.__proto__.__proto__); // { state: false, info: { color: 'red' } } { superClass: [Function: SuperClass], name: 'small car' } { name: 'Car' } console.log(BMW.info); // { color: 'red' } BenZ.info.color = "blue"; console.log(BenZ.name,BenZ.info); // small car { color: 'blue' } console.log(BenZ.name,BMW.info); // small car { color: 'red' } console.log(BMW instanceof SuperClass); // true console.log(BenZ instanceof SuperClass); // true最后總結(jié)一下寄生組合式繼承的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):解決了組合式繼承的父類構(gòu)造函數(shù)調(diào)用兩次的問(wèn)題,只創(chuàng)建了一次父類屬性,并且子類擁有父類原型上的屬性
缺點(diǎn):多繼承問(wèn)題和子類prototype被修改(個(gè)人感覺(jué)后者可以適當(dāng)調(diào)整賦值位置解決,而多繼承問(wèn)題可以考慮使用mixin進(jìn)行優(yōu)化)
看到這里,不知道你是否對(duì)JS繼承有感觸,覺(jué)得它和深復(fù)制有點(diǎn)像
不錯(cuò),JS繼承的類被繼承時(shí),其屬性和行為也會(huì)被復(fù)制到子類中,JavaScript中沒(méi)有類只有對(duì)象,而我們所說(shuō)的類的繼承,實(shí)際上是基于對(duì)象的深復(fù)制
想了解深復(fù)制和寄生組合式繼承的同學(xué)可以跳到這篇文章代碼
總結(jié)
以上就是JS繼承的實(shí)現(xiàn)與使用,感謝你看到了最后,如果這篇文章有幫助到你,請(qǐng)支持一下作者,你的支持是我創(chuàng)作的動(dòng)力
有需要源碼的小伙伴可以看這里(有點(diǎn)亂):myCode: 一些小案例 - Gitee.com
總結(jié)
以上是生活随笔為你收集整理的这一次带你彻底搞懂JS继承的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: (附源码)计算机毕业设计SSM健身俱乐部
- 下一篇: 如何使用JavaScript创建文本搜索