一文看懂JS中的繼承

前言

看懂這篇文章需要你至少了解原型鏈的原理。

直接舉例子,先定義一個Person,再定義一個Man:

function Person(name){
  this.name = name; 
  this.species='person'
  this.have = [1,2,3,4]
}
Person.prototype.hello = () => {
  console.log('person')
}
function Man(name) {
  this.name = name; 
  this.gender='man'
}
Man.prototype.hello = () => {
  console.log('man')
}

原型繼承

我們首先創建a,b兩個實例:

let a = new Person('a')
Person {name: "a", species: "person"}
  name: "a"
  species: "person"
  have: (4) [1, 2, 3, 4]
  __proto__:
    hello: () => {console.log('person')}
    constructor: ƒ Person(name)
    __proto__: Object
let b = new Man('b')
Man {name: "b", gender: "man"}
  name: "b" 
  gender: "man"
  __proto__:
    hello: () => {console.log('man')}
    constructor: ƒ Man(name)
    __proto__: Object

這時我們按照原型繼承的方式:

Man.prototype=a
Man.prototype.constructor=Man

然後我們再創建一個c實例,爲c的have方法推一個新值:

let c = new Man('c')
c.have.push(5)
Man {name: "c", gender: "man"}
name: "c"
gender: "man"
__proto__: Person
  name: "a"
  species: "person"
  have: (5) [1, 2, 3, 4,5]
  constructor: ƒ Man(name)
  __proto__: Object
    hello: () => { console.log('person') }
    constructor: ƒ Person(name)
    __proto__: Object

總結一下原型繼承的缺點:

  1. 子類原型上的方法被覆蓋。
  2. 父類引用類型的屬性會被子類公用。
  3. 子類型無法給父類型傳遞參數

類式繼承

針對上面的原型繼承的問題,我們可以採用類式繼承解決:

function Women(name){
  Person.call(this, name)
  this.gender='women'
}
let e = new Women('e')
name: "e"
species: "person"
have: (4) [1, 2, 3, 4]
gender: "women"
__proto__: Object

這樣就很好的解決了子類型向父類構造函數中傳參的問題,引用屬性也不會被公用。

但是這樣一來,其實問題又更多了,典型的就是沒辦法複用函數。同時Person原型定義的方法,再e中根本無法使用。

組合式繼承

很簡單,融合一下唄。

  1. 使用原型鏈對原型屬性和方法的繼承
  2. 構造函數對於實例屬性的繼承
function Child(name) {
  Person.call(this, name) 
  this.gender='child'
}
Child.prototype.hello = () => {
  console.log('child')
}
Child.prototype=new Person()
let f = new Child('f')
Child {name: "f", species: "person", have: Array(4), gender: "child"}
name: "f"
species: "person"
have: (4) [1, 2, 3, 4]
gender: "child"
__proto__: Person
  name: undefined                  //不需要
  species: "person"                //不需要
  have: (4) [1, 2, 3, 4]           //不需要
  __proto__:
    hello: () => { console.log('person') }
    constructor: ƒ Person(name)
    __proto__: Object

同時我們再次push值給f.have的話:

f.have.push(5)
Child {name: "f", species: "person", have: Array(5), gender: "child"}
name: "f"
species: "person"
have: (5) [1, 2, 3, 4, 5]
gender: "child"
__proto__: Person
  name: undefined
  species: "person"
  have: (4) [1, 2, 3, 4]
  __proto__:
    hello: () => { console.log('person') }
    constructor: ƒ Person(name)
    __proto__: Object

可以看到,haved的push出現在了f上,而它的原型上仍然是四個值的數組。

但是這種做法也是有點缺點的,典型的就是組合繼承使用過程中會父類構造函數被調用兩次。

而顯然,Child.prototype=new Person() 這一次是不需要的,因爲我們獲取實例屬性並不需要通過原型繼承。

這時你可能會想到:

Child.prototype = Person.prototype

的方法,這樣當然也是不對的,雖然可以避免去讀父類實例屬性,但是會把父類和字類的原型合併爲一個。

所以有了下面的方法。

組合寄生

這裏我們用了一部轉化:

function create(proto) {
  function F(){}; 
  F.prototype=proto; 
  return new F()
}
function Nobody(name) {
  Person.call(this, name) 
  this.gender='nobody'
}
Nobody.prototype=create(Person.prototype)
Nobody.prototype.hello=()=>{console.log('nobody')}
Nobody.prototype.constructor=Nobody

和原來的組合繼承相比僅僅改了一處:

Nobody.prototype=create(Person.prototype)
Child.prototype =new Person()

得到的結果就相差極大:

let c = new Nobody('c')
Nobody {name: "c", species: "person", have: Array(4), gender: "nobody"}
name: "c"
species: "person"
have: (4) [1, 2, 3, 4]
gender: "nobody"
__proto__: Person
  hello: ()=>{console.log('nobody')}
  constructor: ƒ Nobody(name)
  __proto__:
    hello: () => { console.log('person') }
    constructor: ƒ Person(name)
    __proto__: Object

能做到這樣主要原因就是我們寫的這個create函數:

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

這個函數中我們創建了一個空的構造函數,把他的prototype屬性指向傳入的參數,最後返回一個該構造函數的實例。這樣可以保證原型鏈的完整性,同時又可以保證實例的原型上沒有多餘屬性。

最後ES5中對這個create其實封裝好了,我們使用時如下即可:

function Nobody(name) {
  Person.call(this, name) 
  this.gender='nobody'
}
Nobody.prototype=Object.create(Person.prototype)
Nobody.prototype.hello=()=>{console.log('nobody')}
Nobody.prototype.constructor=Nobody

創建個實例即:

let h = new Nobody('h')
Nobody {name: "h", species: "person", have: Array(4), gender: "nobody"}
name: "h"
species: "person"
have: (4) [1, 2, 3, 4]
gender: "nobody"
__proto__: Person
  hello: ()=>{console.log('nobody')}
  constructor: ƒ Nobody(name)
  __proto__:
	hello: () => { console.log('person') }
	constructor: ƒ Person(name)
	__proto__: Object

ES6的class語法糖

這就不講了,這還講個啥

發佈了386 篇原創文章 · 獲贊 411 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章