最近回顧 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
}
複製程式碼
原型鏈繼承
原型鏈模式需要手動重新繫結 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 屬性呢
複製程式碼
組合繼承
上面兩者結合即成為組合繼承模式,這個是結合了兩者的優勢,在 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 子類父類是同一個
複製程式碼
原型式繼承
原型式繼承直接使用 ES5 Object.create
方法,該方法的原理是建立一個建構函式,建構函式的原型指向物件,然後呼叫 new 操作符建立例項,並返回這個例項,本質是一個淺拷貝
- ? 父類方法可以複用
- ❌ 父類引用屬性全部被共享
- ❌ 子類不可傳遞引數給父類
let parent = {
name: 'parent',
share: [1, 2, 3], // ❌ 父類的引用屬性全部被子類所共享
log: function() { // ? 父類方法可以複用
return this.name
}
}
let child = Object.create(parent) // ❌ 子類不能向父類傳遞引數
複製程式碼
寄生式繼承
原型式繼承的基礎上為子類增加屬性和方法
- ? 父類方法可以複用
- ? 增加了別的屬性和方法
- ❌ 父類引用屬性全部被共享
- ❌ 子類不可傳遞引數給父類
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 將其轉化一下看看:
小結
最後上個圖作為總結:
參考: