您好,登錄后才能下訂單哦!
這篇文章運用簡單易懂的例子給大家介紹深入淺析js中的繼承,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
JS的“類”
javascript不像java,php等傳統的OOP語言,js本身并沒有類這個概念,那么它是怎么實現類的模擬呢?
構造函數方式
Function Foo (name) { this.name = name this.like = function () { console.log(`like${this.name}`) } } let foo = new Foo('bibidong')
像這樣就是通過構造函數的方式來定義類,其實和普通函數一樣,但為了和常規函數有個區分,一般把函數名首字母大寫。
缺點:無法共享類的方法。
原型方式
function Foo (name) {} Foo.prototype.color = 'red' Foo.prototype.queue = [1,2,3] let foo1 = new Foo() let foo2 = new Foo() foo1.queue.push(4) console.log(foo1) // [1, 2, 3, 4] console.log(foo2) // [1, 2, 3, 4]
我們通過原型方式直接把屬性和方法定義在了構造函數的原型對象上,實例可以共享這些屬性和方法,解決了構造函數方式定義類的缺點。
缺點:可以看到我們改變了foo1的數據,結果foo2的queue屬性也變了,這便是原型方式最大的問題,引用類型的屬性會被其它實例修改。除此之外,這種方式下也無法傳參。
混合方式
function Foo (name) { // 屬性定義在構造函數里面 this.name = name this.color = 'red' this.queue = [1,2,3] } Foo.prototype.like = function () { // 方法定義在原型上 console.log(`like${this.name}`) } let foo1 = new Foo() let foo2 = new Foo()
所謂混合模式,便是把上面兩種方式混合起來,我們在構造函數里面定義屬性,在原型對象上定義要共享的方法,既能傳參,也避免了原型模式的問題。
小結一下:js類的能力是模擬出來的,可以通過構造函數方式,原型方式來定義,混合模式則聚合了前兩者的優點。除此,還有Object.create(), es6的class,都可以來創建對象,定義類。
常見的繼承方式
一、原型鏈繼承
基于原型鏈查找的特點,我們將父類的實例作為子類的原型,這種繼承方式便是原型鏈繼承。
function Parent () { this.color = 'red' this.queue = [1,2,3] } Parent.prototype.like = function () { console.log('') } function Child () { } Child.prototype = new Parent() // constructor指針變了 指向了Parent Child.prototype.constructor = Child // 手動修復 let child = new Child()
Child.prototype相當于是父類Parent的實例,父類Parent的實例屬性被掛到了子類的原型對象上面,拿color屬性舉個例子,相當于就是這樣
Child.prototype.color = 'red'
這樣父類的實例屬性都被共享了,我們打印一下child,可以看到child沒有自己的實例屬性,它訪問的是它的原型對象。
我們創建兩個實例child1,child2
let child1 = new Child() let child2 = new Child() child1.color = 'bulr' console.log(child1) console.log(child2)
我們修改了child1的color屬性,child2沒有受到影響,并非是其它實例擁有獨立的color屬性,而是因為這個color屬性直接添加到了child1上面,它原型上的color并沒有動,所以其它實例不會受到影響從打印結果也可以清楚看到這一點。那如果我們修改的屬性是個引用類型呢?
child1.queue = [1,2,3,'我被修改了'] // 重新賦值 child1.like = function () {console.log('like方法被我修改了')} console.log(child1) console.log(child2)
我們重寫了引用類型的queue屬性和like方法,其實和修改color屬性是完全一樣的,它們都直接添加到了child1的實例屬性上。從打印結果能看到這兩個屬性已經添加到了child1上了,而child2并不會受到影響,再來看下面這個。
child1.queue.push('add push') // 這次沒有重新賦值 console.log(child1) console.log(child2)
如果進行了重新賦值,會添加到到實例屬性上,和原型上到同名屬性便無關了,所以并不會影響到原型。這次我們采用push方法,沒有開辟新空間,修改的就是原型。child2的queue屬性變化了,子類Child原型上的queue屬性被實例修改,這樣肯定就影響到了所有實例。
缺點
二、構造函數式繼承
相當于拷貝父類的實例屬性給子類,增強了子類構造函數的能力
function Parent (name) { this.name = name this.queue = [1,2,3] } Parent.prototype.like = function () { console.log(`like${this.name}`) } function Child (name) { Parent.call(this, name) // 核心代碼 } let child = new Child(1)
我們打印了一下child,可以看到子類擁有父類的實例屬性和方法,但是child的__proto__上面沒有父類的原型對象。解決了原型鏈的兩個問題(子類實例的各個屬性相互獨立、還能傳參)
缺點
三、組合繼承
人如其名,組合組合,一定把什么東西組合起來。沒錯,組合繼承便是把上面兩種繼承方式進行組合。
function Parent (name) { this.name = name this.queue = [1,2,3] } Parent.prototype.like = function () { console.log(`like${this.name}`) } function Child (name) { Parent.call(this, name) } Child.prototype = new Parent() Child.prototype.constructor = Child // 修復constructor指針 let child = new Child('')
接下來我們做點什么,看它組合后能不能把原型鏈繼承和構造函數繼承的優點發揚光大
let child1 = new Child('bibidong') let child2 = new Child('huliena') child1.queue.push('add push') console.log(child1) console.log(child2)
我們更新了child1的引用屬性,發現child2實例沒受到影響,原型上的like方法也在,不錯,組合繼承確實將二者的優點發揚光大了,解決了二者的缺點。組合模式下,通常在構造函數上定義實例屬性,在原型對象上定義要共享的方法,通過原型鏈繼承方法讓子類繼承父類構造函數原型上的方法,通過構造函數繼承方法子類得以繼承構造函數的實例屬性,是一種功能上較完美的繼承方式。
缺點:父類構造函數被調用了兩次,第一次調用后,子類的原型上擁有了父類的實例屬性,第二次call調用復制了一份父類的實例屬性作為子類Child的實例屬性,那么子類原型上的同名屬性就被覆蓋了。雖然被覆蓋了功能上沒什么大問題,但這份多余的同名屬性一直存在子類原型上,如果我們刪除實例上的這個屬性,實際上還能訪問到,此時獲取到的是它原型上的屬性。
Child.prototype = new Parent() // 第一次構建原型鏈 Parent.call(this, name) // 第二次new操作符內部通過call也執行了一次父類構造函數
四、原型式繼承
將一個對象作為基礎,經過處理得到一個新對象,這個新對象會將原來那個對象作為原型,這種繼承方式便是原型式繼承,一句話總結就是將傳入的對象作為要創建的新對象的原型。
先寫下這個有處理能力的函數
function prodObject (obj) { function F (){ } F.prototype = obj return new F() // 返回一個實例對象 } 這也是Object.create()的實現原理,所以用Object.create直接替換prodObject函數是ok的
這也是Object.create()的實現原理,所以用Object.create直接替換prodObject函數是ok的
let base = { color: 'red', queue: [1, 2, 3] } let child1 = prodObject(base) let child2 = prodObject(base) console.log(child1) console.log(child2)
原型式繼承基于prototype,和原型鏈繼承類似,這種繼承方式下實例沒有自己的屬性值,訪問到也是原型上的屬性。
缺點:同原型鏈繼承
五、寄生式繼承
原型式繼承的升級,寄生繼承封裝了一個函數,在內部增強了原型式繼承產生的對象。
function greaterObject (obj) { let clone = prodObject(obj) clone.queue = [1, 2, 3] clone.like = function () {} return clone } let parent = { name: 'bibidong', color: ['red', 'bule', 'black'] } let child = greaterObject(parent)
打印了一下child,它的缺點也很明顯了,寄生式繼承增強了對象,卻也無法避免原型鏈繼承的問題。
缺點
六、寄生組合式繼承
大招來了,寄生組合閃亮登場!
上面說到,組合繼承的問題在于會調用二次父類,造成子類原型上產生多余的同名屬性。Child.prototype = new Parent(),那這行代碼該怎么改造呢?
我們的目的是要讓父類的實例屬性不出現在子類原型上,如果讓Child.prototype = Parent.prototype,這樣不就能保證子類只掛載父類原型上的方法,實例屬性不就沒了嗎,代碼如下,看起來好像是簡直不要太妙啊。
function Parent (name) { this.name = name this.queue = [1,2,3] } Parent.prototype.like = function () { console.log(`like${this.name}`) } function Child (name) { Parent.call(this, name) } Child.prototype = Parent.prototype // 只改寫了這一行 Child.prototype.constructor = Child let child = new Child('')
回過神突然發現改寫的那一行如果Child.prototype改變了,那豈不是直接影響到了父類,舉個栗子
Child.prototype.addByChild = function () {} Parent.prototype.hasOwnProperty('addByChild') // true
addByChild方法也被加到了父類的原型上,所以這種方法不夠優雅。同樣還是那一行,直接訪問到Parent.prototype存在問題,那我們可以產生一個以Parent.prototype作為原型的新對象,這不就是上面原型式繼承的處理函數prodObject嗎
Child.prototype = Object.create(Parent.prototype) // 改為這樣
這樣就解決了所有問題,我們怕改寫Child.prototype影響父類,通過Object.create返回的實例對象,我們將Child.prototype間接指向Parent.prototype,當再增加addByChild方法時,屬性就和父類沒關系了。
寄生組合式繼承也被認為是最完美的繼承方式,最推薦使用。
總結
js的繼承方式主要就這六種,es6的繼承是個語法糖,本質也是基于寄生組合。這六種繼承方式,其中原型鏈繼承和構造函數繼承最為基礎和經典,組合繼承聚合了它們二者的能力,但在某些情況下會造成錯誤。原型式繼承和原型鏈相似,寄生式繼承是在原型式繼承基礎上變化而來,它增強了原型式繼承的能力。最后的寄生組合繼承解決了組合繼承的問題,是一種最為理想的繼承方式。
關于深入淺析js中的繼承就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。