夯基礎-手撕js繼承

提到JS繼承,你首先想到的什麼? 面試 繼承方式 優缺點...,js繼承做爲曾經的苦主,我看了忘,忘了看,看了又忘,OMG,都9012年了面試官還不放過我。javascript

ok,開開玩笑,接下來言歸正傳,來聊聊js繼承這個經典的話題。php

JS的「類」

javascript不像java,php等傳統的OOP語言,js自己並無類這個概念,那麼它是怎麼實現類的模擬呢?java

  1. 構造函數方式
  2. 原型方式
  3. 混合方式

構造函數方式

Function Foo (name) {
    this.name = name
    this.like = function () {
        console.log(`like${this.name}`)
    }
}
let foo = new Foo('bibidong')

像這樣就是經過構造函數的方式來定義類,其實和普通函數同樣,但爲了和常規函數有個區分,通常把函數名首字母大寫。es6

  • 缺點:沒法共享類的方法。

原型方式

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,均可以來建立對象,定義類。this

常見的繼承方式

1、原型鏈繼承

基於原型鏈查找的特色,咱們將父類的實例做爲子類的原型,這種繼承方式即是原型鏈繼承。spa

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屬性舉個例子,至關於就是這樣prototype

Child.prototype.color = 'red'

這樣父類的實例屬性都被共享了,咱們打印一下child,能夠看到child沒有本身的實例屬性,它訪問的是它的原型對象。3d

咱們建立兩個實例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屬性被實例修改,這樣確定就影響到了全部實例。

  • 缺點

    • 子類的實例會共享父類構造函數引用類型的屬性
    • 建立子類實例的時候沒法傳參

2、構造函數式繼承

至關於拷貝父類的實例屬性給子類,加強了子類構造函數的能力

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__上面沒有父類的原型對象。解決了原型鏈的兩個問題(子類實例的各個屬性相互獨立、還能傳參)

  • 缺點

    • 子類沒法繼承父類原型上面的方法和屬性。
    • 在構造函數中定義的方法,每次建立實例都會再建立一遍。

3、組合繼承

人如其名,組合組合,必定把什麼東西組合起來。沒錯,組合繼承即是把上面兩種繼承方式進行組合。

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也執行了一次父類構造函數

4、原型式繼承

將一個對象做爲基礎,通過處理獲得一個新對象,這個新對象會將原來那個對象做爲原型,這種繼承方式即是原型式繼承,一句話總結就是將傳入的對象做爲要建立的新對象的原型。

先寫下這個有處理能力的函數

function prodObject (obj) {
    function F (){
        
    }
    F.prototype = obj
    return new F()  // 返回一個實例對象
}

這也是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,和原型鏈繼承相似,這種繼承方式下實例沒有本身的屬性值,訪問到也是原型上的屬性。

  • 缺點:同原型鏈繼承

5、寄生式繼承

原型式繼承的升級,寄生繼承封裝了一個函數,在內部加強了原型式繼承產生的對象。

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,它的缺點也很明顯了,寄生式繼承加強了對象,卻也沒法避免原型鏈繼承的問題。

  • 缺點

    • 擁有原型鏈繼承的缺點
    • 除此,內部的函數沒法複用

6、寄生組合式繼承

大招來了,寄生組合閃亮登場!

上面說到,組合繼承的問題在於會調用二次父類,形成子類原型上產生多餘的同名屬性。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的繼承是個語法糖,本質也是基於寄生組合。這六種繼承方式,其中原型鏈繼承和構造函數繼承最爲基礎和經典,組合繼承聚合了它們兩者的能力,但在某些狀況下會形成錯誤。原型式繼承和原型鏈類似,寄生式繼承是在原型式繼承基礎上變化而來,它加強了原型式繼承的能力。最後的寄生組合繼承解決了組合繼承的問題,是一種最爲理想的繼承方式。


今天七夕,在線乞討,不要女友只要贊,溜了溜了~

相關文章
相關標籤/搜索