動態

詳情 返回 返回

js繼承您瞭解多少呢 - 動態 詳情

實現繼承的方式有很多,下面我們來寫常用的幾種(包括但不限於原型鏈繼承、構造函數繼承、組合繼承、寄生組合繼承、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為:

image.png
可以看到它的對象上有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為:

image.png

此時原型對象上已經沒有同名的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

user avatar haoqidewukong 頭像 smalike 頭像 chongdianqishi 頭像 razyliang 頭像 leexiaohui1997 頭像 longlong688 頭像 anchen_5c17815319fb5 頭像 huichangkudelingdai 頭像 zero_dev 頭像 woniuseo 頭像 zzd41 頭像 imba97 頭像
點贊 94 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.