JS中「繼承」的合成公式

文前預告

  今天我們來簡單聊聊JS的繼承,我們按這個順序來:原型鏈繼承、借用構造函數繼承、組合繼承原型式繼承、寄生式繼承、寄生組合式繼承。javascript

  爲啥要按這個順序?合成類遊戲每每都把合成原料放在開頭,是吧~java

文前準備

  須要先認識一下「啥是prototype」,「原型鏈是什麼」,說到這,巧了,我上一篇文章講的即是這方面的知識,因此建議先看完《用本身的方式(圖)理解constructor、prototype、__proto__和原型鏈》這篇文章後才繼續往下,固然若是你都足夠了解了,能夠繼續往下。bash

正文開始

下方例子所有都如下面的Parent類爲父類。

function Parent () {
    // 設置私有屬性
    this.nick = '父親',
    this.identity = ['碼農', '奶爸']
}
// Parent放在prototype中的共享方法
Parent.prototype.say = function () {
  console.log('Hello!')
}複製代碼

【合成原料一】原型鏈繼承

// 即將做爲子類
function Child () {}

// 【關鍵語句】將Child構造函數的prototype屬性指向Parent實例對象
Child.prototype = new Parent()

var child = new Child()

// 【效果一】能夠看到Child類的實例對象child繼承了Parent類的屬性
console.log(child.nick) // '父親'
// 【效果二】同時也可以使用父類放在prototype中的共享方法(原型鏈的做用)
child.say() // Hello!複製代碼

【圖片過程展現】ide


【解析】這個不深刻講了,瞭解prototype、原型對象是瞭解大部分繼承的前提,若是看不懂上面一張圖,仍是建議看上面提到的文章,不然下面是看不下去的。函數

【缺陷分析】這種繼承方式被我當成「合成原料」,緣由很簡單,它的缺陷很是明顯,也致使不少人是不會直接拿來就用的。下面舉個列子讓你知道這問題有多嚴重:post

// 假如建立了兩個child實例
var child1 = new Child()
var child2 = new Child()

// 而後只給其中的child2添加了新身份
child2.identity.push('財務')
console.log(child2.identity) // [ '碼農', '奶爸', '財務' ]
console.log(child1.identity) // [ '碼農', '奶爸', '財務' ]

// 【缺陷】什麼?爲何child1中的identity也變了,明明沒有動過呀!
複製代碼

【圖片過程展現】
ui


【解析】能夠看到這種繼承方式使得所謂子類實例出的兩個對象中的__proto__都會指向同一個Parent實例對象,因此只要有一個改變了identity這種引用類型的值,會致使全部實例取得的identity都發生變化。若是你尚未意識到問題的嚴重性,那再舉個例子,你和舍友共用一臺冰箱(裏面吃的也是共享的),結果轉眼間你舍友將零食全吃了,而後你就只剩下一臺空冰箱了。(這裏的冰箱但是引用類型數據)。this

但注意噢,直接賦值不會影響到本身的原型對象,而是直接在對象自身添加該屬性和值。

  因此原型鏈繼承除了你本身確切知道怎麼巧用,別的狀況下仍是要慎重呀。spa

【合成原料二】借用構造函數繼承

  上面原型鏈繼承的明顯缺陷也天然會讓人開始思考:那怎麼可以繼承並擁有獨立屬性呢?prototype

// 有的人在想下面這兩句是給父類添加私有屬性的語句,那要是兒子們都各自調用呢?
this.nick = '父親',
this.identity = ['碼農', '奶爸']複製代碼

  有道理!相信call方法大夥都懂,A.call(B)以B的執行上下文環境執行A的函數執行語句,在咱們這裏呢,其實就是拿父類函數中的執行句子在子類中執行,同時將內部this指向子類。

// 即將做爲子類
function Child() {
  // 【關鍵語句】在Child本身的執行上下文環境中執行Parent方法
  Parent.call(this)
}

// 驗證
var child1 = new Child()
var child2 = new Child()

// 而後只給其中的child2添加了新身份
child2.identity.push('財務')
console.log(child2.identity) // [ '碼農', '奶爸', '財務' ]
console.log(child1.identity) // [ '碼農', '奶爸' ]

// 【效果】讓人興奮,child的實例對象真的有獨立屬性了,之間互不影響!複製代碼

  問題又來了,那Child實例對象仍能使用Parent原型對象中的共享方法嗎?調用看看:

child1.say() // TypeError: child1.say is not a function
// 【缺陷】根本找不到父類放在prototype中的共享方法複製代碼

【圖片過程展現】


【解析】相信瞭解原型鏈的人都知道,實例出來的child1(2)對象的原型對象仍是Child的prototype,這裏面可沒有放着Parent類共享的方法,因此天然找不到,這麼嚴重的缺陷,因此天然這種繼承方式也被我當作合成原料。

  看到這裏有些小夥伴估計想到了,既然原型鏈繼承和借用構造函數繼承的優勢互補(一個方便共享、一個支持私有),那能夠結合着用嗎?是的,的確能夠,而且這還被認爲是一種新的繼承方式——組合繼承。

【中級合成物】組合繼承 = 原型鏈繼承 + 借用構造函數繼承

// 即將做爲子類
function Child() {
  // 【關鍵語句一】在Child本身的執行環境中執行Parent方法(借用構造函數繼承)
  Parent.call(this)
}
// 【關鍵語句二】將Child構造函數的prototype屬性指向Parent實例對象(原型鏈繼承)
Child.prototype = new Parent()

// 驗證
var child1 = new Child()
var child2 = new Child()

// 而後只給其中的child2添加了新身份
child2.identity.push('財務')
console.log(child2.identity) // [ '碼農', '奶爸', '財務' ]
console.log(child1.identity) // [ '碼農', '奶爸' ]

child1.say() // Hello!(這是經過原型鏈找到的共享方法)
// 【效果】讓人興奮,child的實例對象真的有獨立屬性了,之間互不影響!同時共享方法也能用了!複製代碼

  既有獨立屬性又能使用共享方法,該繼承方法多棒!

  真的是嗎?我們來看看它的圖解:

【圖片過程展現】


【解析】優勢我們不講了。問題呢,有沒有發現做爲Child的原型對象的Parent實例對象中的屬性(nick,identity)是多餘的?固然如今所使用的例子問題是不大的,可是若是Parent構造函數中的執行過程很是複雜或者屬性設置很是多,那Parent實例對象的建立開銷就很是大了,真有必要拿這個實例對象做爲原型對象嗎?

  這種繼承方式被做爲中級合成物,是由於其雖帶有小缺陷但用也是沒問題的,不過相信我們必定對高級合成物更感興趣。

  後來有人提出了原型式繼承,給這個問題帶來了解決方法。下面來談談啥是原型式繼承。

【合成原料三】原型式繼承

  這種繼承方式與「原型鏈繼承」差了一個字,實際上的確差異也不大。

  原型式繼承就是拿已有對象做爲原型對象(不徹底對,請繼續往下看)。舉例:

// 【關鍵】存在一個已有對象
var obj = {
  nick: '對象',
 identity: [ '碼農', '奶爸' ],
  say: function () {
    console.log('Hello!')
  }
}
// 即將做爲子類
function Child() {}
// 【關鍵語句】拿已有對象做爲原型對象
Child.prototype = obj

// 驗證
var child = new Child()
console.log(child.nick) // 對象
console.log(child.identity) // [ '碼農', '奶爸' ]
child.say() // Hello!複製代碼

  爲什麼說不徹底對?若是這時改變原型對象內部的屬性,那原有的對象obj就會被影響到,假設我們把這個已有對象當成父類,那子類改值把父類的值給改了,這可不就亂套了!

  因此須要在中間多一層過渡,保護原有對象同時方便擴展。

// 【關鍵】多一層過渡
function inheritObject (o) {
  function F() {}
  F.prototype = o
  // 在這裏方便添加擴展
  F.prototype.a = -1
  // 這樣使得進行屬性操做的是內部的F實例出的對象,而不是原有對象o
  // 同時這能經過原型鏈繼承原有對象o中的共享屬性
  return new F()
}
// 即將做爲子類
function Child() {}
// 【關鍵語句】拿過渡對象做爲原型對象
Child.prototype = inheritObject(obj)

// 驗證
var child = new Child()
console.log(child.nick) // 對象
console.log(child.identity) // [ '碼農', '奶爸' ]
child.say() // Hello!複製代碼

【圖片過程展現】


【解析】因此原型式繼承正確的是在原有對象基礎上構建新對象做爲函數原型。

   可是有些小夥伴很會快指出這種繼承的問題,它和原型鏈繼承一模一樣嘛,而且問題都出在引用屬性的繼承上,一旦修改全部子類都會被影響。因此這也是我將其定義爲合成原料的緣由,不過這種繼承方法但是合成高級合成物的關鍵!

【擴展】Object.create()其實就是inheritObject函數的官方版。

// 第一個參數就是已有函數
var obj = Object.create({}, {
  // 第二個參數用於擴展新的屬性,是一個key:value鍵值對對象
  // 內部屬性定義屬性定義跟Object.definePropeties()方法定義對象的屬性同樣
  a: {
    vwritable: false,
    configurable: true,
    value: -1,
    enumerable: true
  }
})
console.log(obj) // {a: -1}複製代碼

  下面用Object.create()替代inheritObject函數。

【中級合成物】寄生式繼承 = 原型式繼承 + 「調料」

  這個其實沒什麼好講的,其實就是原型式繼承增強版。

var obj = Object.create({})
// 【關鍵】其實就是給原型式繼承生成的過渡對象擴展功能,添加一些共享方法什麼的
function enhance (p) {
  p.jump = function () {
    console.log('jumping')
  }
  // 擴展完就返回擴展後的對象
  return p
}
// 【關鍵語句】拿擴展後的過渡對象做爲原型對象
Child.prototype = obj複製代碼

  因此沒什麼好講,就名字高大上。

【高級合成物】寄生組合式繼承 = 借用構造函數繼承 + 寄生式繼承

  好了,關鍵來了,剛剛有提到說原型式繼承能夠解決組合繼承的缺點:形成沒必要要開銷。

  怎麼解決?先分析問題:

1、子類拿父類實例對象做爲原型對象,這便意味着該原型對象中會含有父類Parent給本身子類的留下的私有屬性;而對於子類而言,在借用構造函數繼承中就已經自行建立了這些私有屬性,因此原型對象中的私有屬性是至關多餘的(懵了回去看組合繼承相關圖解)。

2、若是Parent構造函數執行過程很是複雜,那對於只須要共享數據的咱們來講,建立一個實例對象就是一件得不償失的事情了。

3、Parent構造函數將共享數據就放在它自身的原型對象上,摁~

4、Parent的原型對象在Parent函數聲明時就建立了,能夠直接拿來用,摁?!!!

  這問題不就解決了嗎!Parent的原型對象是已存在的對象,而後又存放着父類Parent的共享數據,這不剛好能用來原型式繼承嗎!並且又不會形成沒必要要開銷,代碼走起:

// 即將做爲子類
function Child() {
  // 【關鍵語句一】在Child本身的執行環境中執行Parent方法(借用構造函數繼承)
  Parent.call(this)
}
// 【關鍵語句二】將Child構造函數的prototype屬性指向基於Parent原型對象構建的新對象(原型式繼承)
Child.prototype = Object.create(Parent.prototype, {
  // 修正由於重寫子類原型對象而致使constructor指向錯誤的問題,上面都沒加,實際上是懶啦,應該加上的
  constructor: {
    vwritable: false,
    configurable: true,
     value: Child,
     enumerable: false
  }
})

// 驗證
var child1 = new Child()
var child2 = new Child()

// 而後只給其中的child2添加了新身份
child2.identity.push('財務')
console.log(child2.identity) // [ '碼農', '奶爸', '財務' ]
console.log(child1.identity) // [ '碼農', '奶爸' ]

child1.say() // Hello!(這是經過原型鏈找到的共享方法)
// 【效果一】讓人興奮,child的實例對象有其獨立屬性了,之間互不影響!而且可以使用共享方法!
console.log(Child.prototype) // Child {}// 【效果二】Child的原型對象上沒有多餘的屬性,也不須要再new一個新的Parent實例了,這少了不少開銷複製代碼

  圖就不畫了,能理解前面的這裏必然也沒有問題。不過其實這裏仍是有點小問題的,名字叫「寄生」組合式繼承,那「寄生」呢?因此其實我們不使用簡單的原型式繼承,而是使用原型式繼承增強版——寄生式繼承。

// 【關鍵】改爲這樣就OK了
function enhance (p) {
  // 在此處擴展...
  return p
}
// 加一個擴展函數
Child.prototype = enhance(
  Object.create(Parent.prototype, {
    // 修正由於重寫子類原型對象而致使constructor指向錯誤的問題
    constructor: {
      vwritable: false,
      configurable: true,
      value: Child,
      enumerable: false
    }
  })
)複製代碼

文後總結

  於我而言,JS中的「繼承」是其高靈活度的產物,這六種繼承方式的設計也是值得咱們好好研究和思索的。難道你不以爲JS這門語言越探索越有意思嗎?反正我以爲是。

  最後你以爲ES6的extends是用哪一種繼承方式的呢?

class Parent {
  constructor() {
    this.nick = '父類'
    this.identity = ['碼農', '奶爸']
  }
  say() {
    console.log('Hello!')
  }
}
class Child extends Parent {
  constructor() {
    super()
  }
}

let child1 = new Child()
let child2 = new Child()
// 【驗證一】看看引用類型屬性是否是公共的 => (有私有屬性)
console.log(child1.identity === child2.identity) // false
// 【驗證二】看看兩個對象中的say方法是否是同一個 => (有共享方法)
console.log(child1.say === child2.say) // true
// 【驗證三】看子類原型對象中有沒有多餘的私有屬性 => (子類原型對象中沒有多餘的私有屬性)
console.log(Child.prototype) // Child {}
// 【驗證四】看看子類原型對象中的__proto__是否是指向Parent原型對象
console.log(Child.prototype.__proto__ === Parent.prototype) // true複製代碼

  你以爲符合哪一種呢?

End

  若有錯漏之處,敬請指正。

相關文章
相關標籤/搜索