實現繼承的方式有很多,下面我們來寫常用的幾種(包括但不限於原型鏈繼承、構造函數繼承、組合繼承、寄生組合繼承、ES6繼承):
原型鏈繼承
原型鏈繼承通過修改子類的原型為父類的實例,從而實現子類可以訪問到父類構造函數以及原型上的屬性或者方法。
// 原型鏈繼承
function Parent () {
this.name = 'kobe'
}
Parent.prototype.getName = function () {
return this.name
}
Parent.prototype.info = {
age: 20,
}
function Child() {}
Child.prototype = new Parent()
let child = new Child()
console.log('child----', child) // Child {}
var child2 = new Child()
console.log('child.info === child2.info', child.info === child2.info) // true
child.getName() // kobe
child2.getName() // kobe
child.info.age = 20
console.log('child1.info.age---', child.info.age) // 20
console.log('child2.info.age---', child2.info.age) // 20 我們很驚奇的發現child2的age也被改掉了
上面的例子是修改的原型對象上的引用數據類型的屬性,如果改成下面這樣,就不會有影響
// 原型鏈繼承
function Parent () {
this.name = 'kobe'
}
Parent.prototype.getName = function () {
return this.name
}
Parent.prototype.info = {
age: 20,
}
function Child() {}
Child.prototype = new Parent()
let child = new Child()
console.log('child----', child) // Child {}
var child2 = new Child()
console.log('child.info === child2.info', child.info === child2.info) // true
child.getName() // kobe
child2.getName() // kobe
child.info = {
age: 18,
num: 24
}
console.log('afterchild === child2', child.info === child2.info) // false
console.log('child1.info---', child.info) // 20 {age: 18, num: 24}
console.log('child2.info---', child2.info) // {age: 20}
因為我們剛開始child的info是引用類型,存的是相同的地址,後面直接給info了一個新對象,相當於生成了一個新對象的地址,兩個info此時指向了不同的地址,互不干擾了
優點
實現邏輯簡單
缺點
父類構造函數中的引用類型(比如對象/數組),會被所有子類實例共享。其中一個子類實例進行修改,會導致所有其他子類實例的這個屬性值都會改變
構造函數繼承
構造函數繼承其實就是通過修改父類構造函數this實現的繼承。我們在子類構造函數中執行父類構造函數,同時修改父類構造函數的this為子類的this。
我們直接看如何實現:
function Parent(name) {
this.name = [name]
}
function Child(name) {
Parent.call(this, name)
}
let child = new Child('kobe')
child.name.push('jordan')
var child2 = new Child('james')
console.log('child---', child.name) // ['kobe', 'jordan']
console.log('child2---', child2.name) // ['james'] 屬性互不影響,但是方法是能各寫各的,方法不通用
優點
解決了原型鏈繼承中構造函數引用類型共享的問題,同時可以向構造函數傳參(通過call傳參)
缺點
所有方法都定義在構造函數中,每次都需要重新創建,方法無法複用(對比原型鏈繼承的方式,方法直接寫在原型上,子類創建時不需要重新創建方法)
所以為了解決原型鏈繼承和構造函數繼承的問題,我們決定把二者優點合一
組合繼承
同時結合原型鏈繼承、構造函數繼承就是組合繼承了。
function Parent () {
this.name='kobe'
}
Parent.prototype.getName = function () {
return this.name
}
function Child () {
Parent.call(this)
this.num = 24
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
let child = new Child()
console.log(child) // {name: 'kobe', num: 24}
console.log('Child.prototype.__proto__ = Parent.prototype', Child.prototype.__proto__ === Parent.prototype) // true
此時child為:
可以看到它的對象上有name屬性,原型對象上也有name屬性
優點
同時解決了構造函數引用類型的問題,同時解決了方法無法共享的問題
缺點
父類構造函數被調用了兩次(第一次是new Parent(), 第二次是Parent.call(this))。同時子類實例以及子類原型對象上都會存在name屬性。雖然根據原型鏈機制,並不會訪問到原型對象上的同名屬性,但總歸是不美。
寄生組合繼承
寄生組合繼承其實就是在組合繼承的基礎上,解決了父類構造函數調用兩次的問題。我們來看下如何解決的:
第一種寫法:
function Parent () {
this.name = 'kobe'
}
Parent.prototype.getName = function () {
return this.name
}
function Child () {
Parent.call(this)
this.num = 24
}
clone(Child, Parent)
function clone (Child, Parent) {
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
}
let child = new Child()
console.log('child-----', child)
第二種寫法:
function Parent () {
this.name = 'kobe'
}
Parent.prototype.getName = function () {
return this.name
}
function Child () {
Parent.call(this)
this.num = 24
}
clone(Child, Parent)
// 這個函數的作用可以理解為複製了一份父類的原型對象
// 如果直接將子類的原型對象賦值為父類原型對象
// 那麼修改子類原型對象其實就相當於修改了父類的原型對象
function clone2 (o) {
function F() {}
F.prototype = o
return new F()
}
function clone (Child, Parent) {
Child.prototype = clone2(Parent.prototype)
Child.prototype.constructor = Child
}
let child = new Child()
console.log('child-----', child)
其實這兩種的本質是一樣的,都是把Parent.prototype給Child.prototype,而不是把Parent給Child.prototype
此時child為:
此時原型對象上已經沒有同名的name屬性了
優點
這種方式就解決了組合繼承中的構造函數調用兩次,構造函數引用類型共享,以及原型對象上存在多餘屬性的問題。是推薦的最合理實現方式(排除ES6的class extends繼承)
缺點
沒有啥特別的缺點
ES6繼承
ES6提供了class語法糖,同時提供了extends用於實現類的繼承,這是項目中最常見的繼承方式。
使用class繼承很簡單,也很直觀:
class Parent {
constructor (name) {
this.name = name
}
getName () {
return this.name
}
}
class Child extends Parent {
constructor (name) {
super(name)
this.num = 24
}
}
const child1 = new Child('kobe')
const child3 = new Child('jordan')
console.log('child1', child1) // {name: 'kobe', num: 24}
console.log('child3', child3) // {name: 'jordan', num: 24}
console.log('child1.getName()', child1.getName()) // kobe
console.log('child3.getName()', child3.getName()) // jordan
補充:
Object.create()方法創建一個新對象,使用現有的對象來提供新創建的對象的__proto__。
const person = {name :'kobe'}
const player = Object.create(person); // player.__proto__ === person
proto必填參數,是新對象的原型對象,如上面代碼裏新對象player的__proto__指向person。注意,如果這個參數是null,那新對象就徹徹底底是個空對象,沒有繼承Object.prototype上的任何屬性和方法,如hasOwnProperty()、toString()等。
技術溝通交流歡迎+V:yinzhixiaxue