關於構造函數、原型、原型鏈、多種方式繼承

構造函數與實例對象

又是這個經典的問題,嗯,我先來寫個構造函數,然後實例化一個對象看看。

function Person(name) {
  this.name = name
}
Person.prototype.eat = () => {console.log('eat')}
Person.prototype.play = () => {console.log('play')}
let Han = new Person('Han')

通過一系列打印發現了這樣的關係:
圖片描述

原型鏈 -- 原型(prototype)和隱式原型(__proto__)

可以看出實例對象沒有prototype(也就是原型),只有構造器才擁有原型。而所有的js對象都擁有__proto__(也就是隱式原型),這個隱式原型所指向的就是創造這個對象的構造器的原型。如實例Han的隱式原型指向了其構造函數(Person)的原型;Person的隱式原型指向了Function的原型;而原型自身也有隱式原型,指向了Object的原型。

有點繞口,其實就是通過隱式原型可以向上找到是誰構造了自己,並且如果自己沒有相應的屬性或者方法,可以沿着這條原型鏈向上找到最近的一個屬性或方法來調用。如Han.eat(),實際上是調用了Han.__proto__.eat(),把構造器Person的原型的eat方法給拿來用了;再如Han.hasOwnProperty('name'),實際上是調用了Han.__proto__.__proto__.hasOwnProperty('name'),因爲Han自己沒hasOwnProperty這方法,就通過隱式原型向上找到了Person的原型,發現也沒這方法,就只能再沿着Person的原型的隱式原型向上找到了Object的原型,嗯然後發現有這方法就拿來調用了。

構造器constructor

所有構造函數都有自己的原型(prototype),而原型一定有constructor這麼個屬性,指向構造函數本身。也就是告訴大家這個原型是屬於本構造函數的。

Function & Object

可以看出Person這個構造函數是由Function創建出來的,而我們看下Function的隱式原型,竟然是指向了Function的原型,也就是Function也是由Function創建出來的。很繞是不是,我們先不管,繼續溯源下去,再看下Function的原型的隱式原型,指向的是Object的原型,繼續往上找Object的原型的隱式原型,嗯終於結束了找到的是null,也就是Object的原型是原型鏈上的最後一個元素了。

接下來看下Object,Object是由Function創建出來的,而Function的隱式原型的隱式原型是Object的原型也就是Function通過原型鏈可以向上找到Object的原型,兩者看起來是你生我我生你的關係,這裏也就引用比較好懂的文章來解釋下: 從Object和Function說說JS的原型鏈

Object
JavaScript中的所有對象都來自Object;所有對象從Object.prototype繼承方法和屬性,儘管它們可能被覆蓋。例如,其他構造函數的原型將覆蓋constructor屬性並提供自己的toString()方法。Object原型對象的更改將傳播到所有對象,除非受到這些更改的屬性和方法將沿原型鏈進一步覆蓋。

Function
Function 構造函數 創建一個新的Function對象。 在 JavaScript 中, 每個函數實際上都是一個Function對象。

---- 來自mozilla

接下來說下構造函數實例化對象到底做了些啥,其實看也能看出來了。

let Jan = {}
Person.call(Jan, 'Jan')
Jan.__proto__ = Person.prototype

1、創建一個空對象。
2、將構造函數的執行對象this賦給這個空對象並且執行。
3、把對象的隱式原型指向構造函數的原型。
4、返回這個對象

是的就是這樣,next page!

繼承

原型鏈繼承

function Person(name) {
  this.name = name
  this.skills = ['eat', 'sleep']
}
Person.prototype.say = ()=> {console.log('hi')}

function Boss() {}
Boss.prototype = new Person()
let Han = new Boss()

原理就是這樣👇
圖片描述
子構造函數的原型指向了父構造函數的實例對象,因此子構造函數的實例對象可以通過原型鏈找到父構造函數的原型方法和類屬性。

優點:
所有實例對象都可以共享父構造函數的原型方法。

缺點:
1、父構造函數的引用屬性也被共享了,相當於所有的實例對象只要對自身的skills屬性進行修改都會引發共振,因爲其實修改的是原型鏈上的skills屬性。當然對skills重新賦值可以擺脫這一枷鎖,相當於自身新建了skills屬性來覆蓋了原型鏈上的。
2、實例化時無法對父構造函數傳參。
3、子構造函數原型中的constructor不再是子類自身,而是通過原型鏈找到了父類的constructor。

構造函數繼承

function Person(name) {
  this.name = name
  this.skills = ['eat', 'sleep']
}
Person.prototype.say = ()=> {console.log('hi')}

function Boss(name) {
  Person.call(this, name)
}
let Han = new Boss('Han')

原理就是父構造函數把執行對象賦給子構造函數的實例對象後執行自身。

優點:
1、實例化時可以對父構造函數傳參。
2、父類的引用屬性不會被共享。
3、子構造函數原型中的constructor還是自身,原型沒有被修改。

缺點:
每次實例化都執行了一次父構造函數,子類不能繼承父類的原型,如果把父類原型上的方法寫在父類的構造函數裏,雖然子類實例對象可以調用父類的方法,但父類的方法是單獨加在每個實例對象上,會造成性能的浪費。

組合繼承

結合了原型鏈繼承和構造函數繼承兩種方法。

function Person(name) {
  this.name = name
  this.skills = ['eat', 'sleep']
}
Person.prototype.say = ()=> {console.log('hi')}

function Boss(name, age) {
  Person.call(this, name)
  this.age = age
}

Boss.prototype = new Person()
Boss.prototype.constructor = Boss
Boss.prototype.sleep = ()=> {console.log('sleep')}

let Han = new Boss('Han', 18)

看起來是完美解決了一切。但就是👇

clipboard.png

實例化的對象實際上是用構造函數繼承的方法往自己身上加屬性從而覆蓋原型鏈上的相應屬性的,既然如此,爲什麼不直接那父構造器的原型加到子構造器的原型上呢?這樣就不會出現那多餘的父類實例化對象出來的屬性了。

function Person(name) {
  this.name = name
  this.skills = ['eat', 'sleep']
}
Person.prototype.say = ()=> {console.log('hi')}

function Boss(name, age) {
  Person.call(this, name)
  this.age = age
}

Boss.prototype = Person.prototype  //modified
Boss.prototype.constructor = Boss
Boss.prototype.sleep = ()=> {console.log('sleep')}

let Han = new Boss('Han', 18)

clipboard.png

看起來很是完美,反正效果是達到了,性能優化也是最佳。但問題就是這樣一點繼承關係都看不出來啊,父類和子類的原型完全融合在一塊了,一點都不嚴謹。

所以最優的繼承方式應該是。。。

寄生組合繼承

function Person(name) {
  this.name = name
  this.skills = ['eat', 'sleep']
  console.log('this',this)
}
Person.prototype.say = ()=> {console.log('hi')}

function Boss(name, age) {
  Person.call(this, name)
  this.age = age
}
Boss.prototype = Object.create(Person.prototype)
Boss.prototype.sleep = ()=> {console.log('sleep')}
Boss.prototype.constructor = Boss
let Han = new Boss('Han', 18)

先看圖👇
圖片描述
其實跟組合繼承有點像,構造函數繼承部分和組合繼承的一樣就不說了。原型鏈那塊和原型鏈繼承有所不同的是原型鏈是直接拿了父類的實例對象來作爲子類的原型,而這裏是用以父類的原型爲原型的構造函數實例化出來的對象作爲子類的原型(Object.create做的事情),完美避開了不必要的父類構造函數裏的東西。

Object.create()方法創建一個新對象,使用現有的對象來提供新創建的對象的__proto__。

相當於這樣👇

function create(proto) {
  function F() {}
  F.prototype = proto
  return new F()
}

聽說ES6的class extend也是這麼做的,更多的繼承細節可以看看這篇文章,本繼承章節也參考了的👇
一篇文章理解JS繼承——原型鏈/構造函數/組合/原型式/寄生式/寄生組合/Class extends

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章