日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

这一次带你彻底搞懂JS继承

發布時間:2023/12/29 javascript 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 这一次带你彻底搞懂JS继承 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄

前言

起步

"new" 究竟發生了什么?

類式繼承(原型鏈繼承)

構造函數繼承

組合繼承

原型式繼承

寄生式繼承

寄生組合式繼承

總結


前言

這段時間復習JS從看懂到看開(前端面試題整合)_DieHunter1024的博客-CSDN博客時發現對繼承概念又陌生了,平時大多用的都是extends,對底層知識難免會生疏,于是決定分享這篇文章,重新學習一下繼承。

起步

JavaScript和面向類的語言不同,它沒有類做對象的抽象模式,它能夠不通過類直接創建對象,相比其他的面向對象語言,JavaScript才能算是真正的面向 " 對象 " 語言。在面向類的語言中構造函數通常是屬于類的,而JavaScript中(在ES6之前),類是屬于構造函數的,為什么這么說?因為我們使用的類實際上是用構造函數實現的。下面進入主題讓我們聊聊繼承。

繼承作為面向對象程序設計特征之一,必定有其重要的意義

繼承是指:在已存在的類的基礎上,拓展出新的類。那么存在的類就是父類,或基類,超類;新的類就是子類,或派生類

其重要意義就是使代碼可以復用,子類中也擁有父類的屬性和方法,從父類一級一級往下,屬性和函數由泛化到細化

那么js中的繼承又是怎樣的呢?

"new" 究竟發生了什么?

要了解繼承,得先了解new,我們在node環境下看看以下案例

JavaScript中的類在es6之前,沒有class語法糖時,用的是構造函數實現的,與class不同的是,構造函數既是類,也是函數,既可以使用 "函數名()" 的方式執行,也可以采用?"new 函數名()" 的方式執行,這二者之間的效果卻是截然不同,下面的例子中,使用 "函數名()" 的方式執行打印的是小暗,而另一個使用new的卻打印了小明(這里我們是在node環境下執行的,function中的this指向的是全局的global,如果是在瀏覽器控制臺執行,就需要把global換成window),由此可以得知 new 實際上是把構造函數原型(prototype)上的屬性放在了原型鏈(__proto__)上,那么當實例化對象取值時就會在原型鏈上取,而實例化對象上的prototype已經不見了

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

我們可以簡單理解為 new 實際上是將構造函數的prototype上的屬性放在了實例化對象的__proto__上 ,通過實例化對象 . 屬性名進行取值

那么new如何實現呢?

來看看下面的代碼

exports.newClass = function () {const _target = new Object(); // 新增一個容器,用來裝載構造函數(目標類)prototype上的所有屬性const _this = this; //不能直接通過 this() 來運行構造函數,所以用一個變量裝載_target.__proto__ = _this.prototype; // 核心部分:將構造函數prototype上的所有屬性放到新容器中const result = _this.apply(_target, arguments); // 執行構造函數,相當于執行class中的constructorreturn result && typeof result === "object" ? result : _target; // 若函數返回值為引用類型返回當前函數執行結果,否則將新的容器返回,此時通過 _target[屬性名]就可以訪問 this.prototype 中的屬性了 };

上述代碼將 new 實現了一下,其中最重要的一步就是將構造函數prototype上的所有屬性放到新容器中,最后獲得的實例化對象的__proto__上就有了構造函數原型中所有屬性了,下面我們放在之前的代碼中看看效果

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

說了這么多,其實目的是為了讓大家知道:實例化一個構造函數,實際上可以簡單理解為將類的prototype上的屬性轉移到實例化對象中,這樣有助于理解后續的繼承的實現,話不多說,直接開始

類式繼承(原型鏈繼承)

結合?new?的原理可以知道:?類式繼承實際上是通過?new?將?SuperClass.prototype?綁定到?SuperClass.__proto__?上,然后賦值給?SubClass.prototype,當實例化?SubClass?時,SubClass.__proto__?上也會帶有?SuperClass?及其原型鏈上的屬性,即?SubClass?實例化對象上有以下屬性:SuperClass.prototype?上的屬性(實例化對象.__proto__.__proto__),SuperClass?構造函數上的屬性(實例化對象.__proto__),SubClass?構造函數上的屬性(實例化對象)

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

優點:簡潔方便,子類擁有父類及父類?prototype?上屬性

缺點:

  • 子類通過prototype繼承父類,只能父類單向傳遞屬性給子類,無法向父類傳遞參數。為什么要向父類傳遞參數?如果父類中的某屬性對參數有依賴關系,此時子類繼承父類就需要在?new?SuperClass()?時傳參
  • 當父類原型上的引用屬性改變時,所有子類實例相對應的引用屬性都會對應改變,即繼承的引用類型屬性都有引用關系
  • 子類只能繼承一個父類(因為繼承方式是直接修改子類的prototype,如果再次修改,會將其覆蓋)
  • 繼承語句前不能修改子類的?prototype?因為此類繼承會覆蓋子類原型

構造函數繼承

在?SubClass?構造函數中使用?SuperClass.call?直接運行?SuperClass?構造函數,然而直接執行構造函數和使用?new?實例化構造函數二者是完全不同的:

  • 前者(直接執行構造函數)在下方代碼中會將?SuperClass?構造函數里初始化的屬性帶到?SubClass?中,而?SuperClass.prototype?中的?name?屬性并未帶到?SubClass?中;

  • 而后者(使用?new?實例化構造函數)則會將?SuperClass.prototype?中的屬性帶到?SuperClass?實例化對象的?__proto__?上

function SuperClass(props) {this.state = props;this.info = { color: "red" }; } SuperClass.prototype = {name: "Car", }; function SuperClass2() {this.size = 'small'; } // 注意:構造函數使用 call 會重寫子類同名屬性,要寫在子類的最開始 function SubClass() {SuperClass.call(this, ...arguments);SuperClass2.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__); // SubClass { // state: true, // info: { color: 'red' }, // size: 'small', // price: 1000 // } SubClass { name: 'Small 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) // false

所以其優點是:

  • 可以在?SuperClass?執行時傳參數

  • 可以繼承多個父類

  • 繼承同一個父類的子類的屬性之間不會有引用關系(因為父類構造函數的執行是在每個子類中call(this)了,從而在父類構造函數執行時,this分別代表著每個子類)

缺點是:父類 prototype 上的屬性無法繼承,只能繼承父類構造函數的屬性,正是因為這點,父類的函數無法復用(指無法復用父類?prototype?中的函數,只能通過父類構造函數將函數放在子類中)

針對父類的函數無法復用的理解:

父類 SuperClass?每次在子類?SubClass?中執行都會在每個子類重新初始化?this.屬性?或?this.函數,這些屬性是屬于每個子類單獨的,這樣既增加了性能負擔又使父類原型中的公共屬性無法復用;

而倘若這些函數或者屬性在?SuperClass?的?prototype?上,并且子類能繼承父類,則所有子類用公共屬性的都是父類的,此時就達到了復用效果,而類式繼承卻能夠實現這個效果,于是就有了下面的組合繼承

組合繼承

構造函數繼承不能繼承父類原型上的屬性,而類式繼承無法傳參給父類,組合繼承正好將兩者規避了

然而組合繼承在實例化父類和執行父類構造函數時執行了兩次?SuperClass?,實際上類式繼承是為了解決構造函數繼承上的父類的?prototype?無法被子類繼承的問題,看代碼可以得知,new?SuperClass()?確實會將父類的?prototype?繼承到子類中,但是也會將?SuperClass?構造函數中的操作又執行一遍(具體可看?console.log(++count)?執行了3次),而且類式繼承是將子類的原型直接替換掉,所以無法繼承多個父類的問題也被延續下來了(但是可以在父類上多加一次繼承,使多個類形成原型鏈關系,達到多繼承的目的,即A,B,C三個類,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

優點:解決類式繼承和構造函數繼承的主要問題

缺點:父類構造函數執行兩遍,性能損耗

原型式繼承

原型式繼承是基于類式繼承的封裝,特點和類式繼承一樣,繼承的引用類型屬性都有引用關系

原型式繼承的過渡對象F實際上就是類式繼承中的子類構造函數,這么做相比類式繼承的特點:減少性能開銷(子類是空白的構造函數,沒有任何內容),對應的,無法在子類構造函數中初始化屬性

是不是覺得原型式繼承和?Object.create( )?很像? create 函數的原理就是生成一個新對象,這個新對象的 __proto__ 等于傳入的對象。讓我們回憶一下前面講到的 new 的原理,new 實際上就是將?prototype 放在實例化對象的?__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

類式繼承是如何轉換成原型式繼承?看以下代碼是不是清晰了一點,所以原型式繼承也可以寫成const?subClass=Object.create(superClass)

function prototypeInheritance(SuperClass) {function SubClass() {}SubClass.prototype = new SuperClass();return new SubClass(); }

優點:無子類構造函數開銷,相當于實現了對象的淺復制

缺點:?

  • 繼承時無法向父類傳參
  • 和類式繼承一樣,繼承父類的引用類型屬性都有引用關系

寄生式繼承

寄生式繼承實際上是在上面的原型式繼承的基礎上做了二次封裝,可以看成工廠模式+原型式繼承,將繼承步驟放在新的函數中,此時便可以在子類構造函數上添加子類獨有的函數和屬性,由此叫做寄生式繼承,就好像子類獨有的屬性方法寄生在下面的?parasiticInheritance?函數中一樣。使用這種繼承在新建子類時,每個子類中的屬性都不一樣,違背了代碼復用的效果

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 說明每個子類的屬性都不一樣 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

優點:

  • 無子類構造函數開銷

  • 繼承父類所有屬性

  • 子類擁有自己的屬性

缺點:

  • 繼承時無法向父類傳參

  • 和類式繼承一樣,繼承父類的引用類型屬性都有引用關系

  • 子類公共屬性無法在原型上定義,導致無法復用

針對代碼無法復用缺點的理解:讓我們回憶一下上面的構造函數繼承對代碼復用的理解,子類構造函數中直接執行父類構造函數并改變?this?指向從而達到將父類屬性初始化到子類中。而寄生式繼承則是每次生成的子類都是新的構造函數?F?,所以在繼承時單獨給?subClass?增加屬性實際上是操作不同的子類構造函數,而如果這個做法能在子類?prototype?中進行,那么子類的函數及屬性可以復用。

寄生組合式繼承

實際上上述繼承方式都是實現最終繼承方式的猜想和嘗試,在ES6的class語法糖出現之前,寄生組合式繼承是最理想的繼承方式,下面讓我們來看看

顧名思義寄生組合式繼承就是寄生式繼承和組合式繼承的結合,個人認為叫它寄生組合式繼承倒不如稱其為原型組合式繼承,因為他的寫法就是原型式繼承+組合式繼承

作為ES6之前最理想的繼承,我們當然是要深入分析一下,這么做到底好在哪?

我們按照標題寄生組合式繼承實現一下這種繼承的寫法

// 之前寫的原型式繼承 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);

乍一看,這種寫法和組合式繼承屬實有點像,但是有一點不同:
prototypeInheritance 函數會生成一個只包含父類原型上屬性而沒有執行父類構造函數的 “純凈” 的新對象(即不執行父類構造函數)。
這句話怎么理解?
讓我們結合一下 new 的原理,回憶一下類式繼承或組合式繼承是如何實現的:SubClass.prototype?=?new?SuperClass() 這樣會導致子類 prototype 中既執行了父類構造函數,也有父類原型上的屬性。而實際上我們是暫時不需要執行父類構造函數的,因為在組合式繼承中還有一步:在子類中執行 SuperClass.call(this, ...arguments) ,這一步會將父類構造函數再執行一次,將其二者結合,于是我們就得到了組合式繼承的升級版:寄生組合式繼承

我們將prototypeInheritance簡寫成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); //調用一下父類構造函數,將父類的屬性放在子類中 }parasiticCombinatorialInheritance(SuperClass, SubClass); SubClass.prototype.name = "small car";//修改prototype值寫在繼承后面 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

最后總結一下寄生組合式繼承的優缺點

優點:解決了組合式繼承的父類構造函數調用兩次的問題,只創建了一次父類屬性,并且子類擁有父類原型上的屬性

缺點:多繼承問題和子類prototype被修改(個人感覺后者可以適當調整賦值位置解決,而多繼承問題可以考慮使用mixin進行優化)

看到這里,不知道你是否對JS繼承有感觸,覺得它和深復制有點像

不錯,JS繼承的類被繼承時,其屬性和行為也會被復制到子類中,JavaScript中沒有類只有對象,而我們所說的類的繼承,實際上是基于對象的深復制

想了解深復制和寄生組合式繼承的同學可以跳到這篇文章代碼

總結

以上就是JS繼承的實現與使用,感謝你看到了最后,如果這篇文章有幫助到你,請支持一下作者,你的支持是我創作的動力

有需要源碼的小伙伴可以看這里(有點亂):myCode: 一些小案例 - Gitee.com

總結

以上是生活随笔為你收集整理的这一次带你彻底搞懂JS继承的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。