? 一文看懂 JS 繼承

JS菌發表於2019-03-17

20190317203439.png

最近回顧 js 繼承的時候,發現還是對一些概念不是很清晰。這裡再梳理一下 JS 中繼承的幾種主要的方式,建構函式繼承、原型鏈繼承、組合繼承以及原型式繼承、寄生式繼承、寄生組合式繼承和 ES6 的 Class:

建構函式繼承

建構函式繼承沒有用到 prototype 這種方式比較常見,定義和使用也較為簡單,下面是一個例子?:

  • ? 可以定義私有屬性方法
  • ? 子類可以傳遞引數給父類
  • ❌ 不能定義共享屬性方法/或寫在外面失去了封裝性
function Parent(name, friends) {
    this.name = name
    this.friends = friends // ? 可以定義私有 引用型別不會被共享
    this.share = share // ❌ 可以定義公有 但需要放在外部
    this.log = log // ❌ 避免重複宣告,為了複用需要放在外面
}
// ❌ 公有屬性和方法定義在外面失去了封裝性
let share = [1, 2, 3]
function log() {
    return this.name
}

function Child(name, friends, gender) {
    Parent.call(this, name, friends) // ? 可以在子類傳遞引數給父類
    this.gender = gender
}
複製程式碼

20190317153141.png

原型鏈繼承

原型鏈模式需要手動重新繫結 constructor 而且不能定義私有變數

  • ? 可以定義公有屬性方法
  • ❌ 無論是定義還是繼承都需要手動修改 constructor
  • ❌ 封裝性一般
  • ❌ 不能定義私有屬性方法
  • ❌ 沒辦法向父類傳遞引數
function Parent() {}
Parent.prototype = {
    constructor: Parent, // ❌ 需要手動繫結 constructor
    name: 'oli', // ❌ 不能定義私有屬性,全部都是公有
    friends: ['alice', 'troy'], // ? 可以定義公有屬性 所有例項都引用這個
    log: function() { // ? 方法被共享了
        return this.name
    }
}
// 也可以寫成多個 Parent.prototype.func1 = function(){} 封裝性更差 但不用修改 constructor
// ❌ 封裝性一般

function Child() {} // ❌ 沒辦法向父類傳遞引數
Child.prototype = new Parent() // 使用 new 操作符建立並重寫 prototype
Child.prototype.constructor = Child // ❌ 每次繼承都需要手動修改 constructor 誰叫你是覆蓋 prototype 屬性呢
複製程式碼

20190317164208.png

組合繼承

上面兩者結合即成為組合繼承模式,這個是結合了兩者的優勢,在 ES6 的 class 出現之前的常用方法,??看看例子:

  • ? 公有的寫在原型
  • ? 私有的寫在建構函式
  • ? 可以向父類傳遞引數
  • ❌ 需要手動繫結 constructor
  • ❌ 封裝性一般
  • ⚡ 重複呼叫父類效能損耗
function Parent(name, friends) {
    // ? 私有的寫這裡
    this.name = name // ? 可以定義私有屬性
    this.friends = friends // ? 可以定義公有引用屬性不會被共享
}
Parent.prototype = {
    // ? 公有的寫這裡
    constructor: Parent, // ❌ 需要手動繫結 constructor
    share: [1, 2, 3], // ? 這裡定義的公有屬性會被共享
    log: function() { // ? 方法被共享了
        return this.name
    }
}
// ❌ 封裝性一般

function Child(name, friends, gender) {
    Parent.call(this, name, friends) // ? 可以向父類傳遞引數 ⚡ 這裡又呼叫了一次 Parent
    this.gender = gender
}
Child.prototype = new Parent() // 使用 new 操作符建立並重寫 prototype ⚡ 這裡呼叫了一次 Parent
// 有方法避免多次呼叫直接去掉 new 操作符 轉而寫成 Child.prototype = Parent.prototype 這樣並不好,雖然避免出現重複呼叫但導致修改子類 constructor 的時候父類也被修改了
Child.prototype.constructor = Child // ❌ 每次繼承都需要手動修改 constructor 誰叫你是覆蓋 prototype 屬性呢
// 如果使用 Child.prototype = Parent.prototype 那麼 constructor 子類父類是同一個
複製程式碼

20190317171213.png

原型式繼承

原型式繼承直接使用 ES5 Object.create 方法,該方法的原理是建立一個建構函式,建構函式的原型指向物件,然後呼叫 new 操作符建立例項,並返回這個例項,本質是一個淺拷貝

  • ? 父類方法可以複用
  • ❌ 父類引用屬性全部被共享
  • ❌ 子類不可傳遞引數給父類
let parent = {
    name: 'parent',
    share: [1, 2, 3], // ❌ 父類的引用屬性全部被子類所共享
    log: function() { // ? 父類方法可以複用
        return this.name
    }
}

let child = Object.create(parent) // ❌ 子類不能向父類傳遞引數
複製程式碼

20190317183345.png

寄生式繼承

原型式繼承的基礎上為子類增加屬性和方法

  • ? 父類方法可以複用
  • ? 增加了別的屬性和方法
  • ❌ 父類引用屬性全部被共享
  • ❌ 子類不可傳遞引數給父類
let parent = {
    name: 'parent',
    share: [1, 2, 3],
    log: function() {
        return this.name
    }
}

function create(obj) {
    let clone = Object.create(obj) // 本質上還是 Object.create
    clone.print = function() { // 增加一些屬性或方法
        console.log(this.name)
    }
    return clone
}

let child = create(parent)
複製程式碼

寄生組合式繼承

雜糅了原型鏈式、建構函式式、組合式、原型式、寄生式而形成的一種方式:

組合繼承的方法會呼叫兩次 Parent,一次是在 Child.prototype = new Parent() ,一次是在 Parent.call()。這個是組合繼承的唯一缺點,寄生組合式解決了這個問題:

  • ? 公有的寫在原型
  • ? 私有的寫在建構函式
  • ? 可以向父類傳遞引數
  • ? 不會重複呼叫父類
  • ❌ 需要手動繫結 constructor (如果重寫 prototype)
  • ❌ 需要呼叫額外的方法封裝性一般
function Parent(name, friends) {
    this.name = name
    this.friends = friends
}
Parent.prototype = {
    constructor: Parent, // ❌ 需要手動繫結 constructor
    share: [1, 2, 3],
    log: function() {
        return this.name
    }
}

function Child(name, friends, gender) {
    Parent.call(this, name, friends) // ⚡ 這裡只需要呼叫一次 Parent
    this.gender = gender
}
// 上半部分和組合繼承一樣

let F = function() {} // 建立一箇中介函式
F.prototype = Parent.prototype // 這個中介的原型指向 Parent 的原型
Child.prototype = new F() // 注意這裡沒有使用 new 操作符呼叫 Parent
Child.prototype.constructor = Child
複製程式碼

對上述方法進行一個封裝:

function Parent(name, friends) {
    this.name = name // ? 可以定義私有屬性
    this.friends = friends // ? 可以定義公有引用屬性不會被共享
}
Parent.prototype = {
    constructor: Parent, // ❌ 需要手動繫結 constructor
    share: [1, 2, 3], // ? 這裡定義的公有屬性會被共享
    log: function() { // ? 方法被共享了
        return this.name
    }
}

function Child(name, friends, gender) {
    Parent.call(this, name, friends) // ? 可以向父類傳遞引數 ⚡ 這裡又呼叫了一次 Parent
    this.gender = gender
}

function proto(child, parent) {
    let clonePrototype = Object.create(parent.prototype)
    child.prototype = clonePrototype
    child.prototype.constructor = child
}

proto(Child, Parent)
複製程式碼

ES6 class

class 的語法,就比較清晰了,能用 class 就用 class 吧:

class Parent {
    constructor(name, friends) { // 該屬性在建構函式上,不共享
        this.name = name
        this.friends = friends
    }
    log() { // 該方法在原型上,共享
        return this
    }
}
Parent.prototype.share = [1, 2, 3] // 原型上的屬性,共享

class Child extends Parent {
    constructor(name, friends, gender) {
        super(name, friends)
        this.gender = gender
    }
}
複製程式碼

另外可以使用 get set 方法將 share 屬性寫入到原型中去

另外,class 是一種語法糖使用 babel 將其轉化一下看看:

20190317201244.png

小結

最後上個圖作為總結:

20190317202656.png

參考:

? 一文看懂 JS 繼承

相關文章