JS 繼承的實現

JS從誕生之初本就不是面向對象的語言。

如何在JS中實現繼承,總結而言會有四種寫法。

構造函數繼承

function Animal(name) {
    this.name = name
        
    this.sayName = function() {
        console.log(this.name)
    }
}
    
function Dog(name, hobby) {
    // 遍歷
    let ani = new Animal(name)
    for(let p in ani) {
        if (ani.hasOwnProperty(p)) {
            this[p] = ani[p]
        }
    }
        
    this.hobby = hobby
}
    
let dog1 = new Dog('xiaohei', 'bone')
let dog2 = new Dog('fofo', 'bone and fish')
console.log(dog1.sayName()) // xiaohei
console.log(dog2.sayName()) // fofo

通過對象冒充實現繼承,實際上是在構造函數中,通過獲取父類中的所有屬性,並保存到自身對象中,這樣則可以調用父類的屬性和方法了。這裏forin的方式遍歷父類屬性,因爲forin會遍歷公開的屬性和方法,所以通過hasOwnProperty控制寫入當前對象的範圍。否則則會將所有屬性全部變爲私有屬性。

這樣做有一個缺點就是,無法訪問父類中的公開方法和屬性(prototype中的方法)

Animal.prototype.sayHobby = function() {
    console.log(this.hobby)
}
dog1.sayHobby() // VM2748:1 Uncaught TypeError: dog1.sayHobby is not a function at <anonymous>:1:6

代碼優化

在子類中,既然是需要獲取父類的私有屬性,則可以使用callapply,當調用父類的方法的時候,改變當前上下文爲子類對象,則子類對象就可以獲取到了父類的所有私有屬性。

function Animal(name) {
    this.name = name
        
    this.sayName = function() {
        console.log(this.name)
    }
}
    
function Dog(name, hobby) {
    // 更改構造函數的上下文
    Animal.call(this, name)
    
    this.hobby = hobby
}
    
let dog1 = new Dog('xiaohei', 'bone')
let dog2 = new Dog('fofo', 'bone and fish')
console.log(dog1.sayName()) // xiaohei
console.log(dog2.sayName()) // fofo

類式繼承

function Animal(name) {
    this.name = name || 'animal'
    this.types = ['cat', 'dog']
    
    this.sayTypes = function() {
        console.log(this.types.join('-'))
    }
}
Animal.prototype.sayName = function() {
    console.log(this.name)
}

function Dog(name) {
    this.name = name    
}
Dog.prototype = new Animal('animal')

let dog1 = new Dog('xiaohei')
dog1.sayName() // xiaohei

let dog2 = new Dog('feifei')
dog2.sayName() // feifei

這種繼承方式是通過對子類的prototype.__proto__引用父類的prototype,從而可以讓子類訪問父類中的私有方法和公有方法。詳情可以查看關鍵字new的實現。

類式繼承會有兩方面的缺點

  1. 引用陷阱-子類對象可以隨意修改父類中的方法和變量,並影響其他子類對象

    dog1.types.push('fish')
    console.log(dog1.types) // ["cat", "dog", "fish"]
    console.log(dog2.types) // ["cat", "dog", "fish"]

  2. 無法初始化構造不同的實例屬性

這個主要是由於類式繼承,是通過Dog.prototype = new Animal('animal')實現的,我們只會調用一次父類的構造函數。所以只能在子類中從寫父類的屬性,如上的name屬性,在子類中需要重寫一次。

組合繼承

組合繼承,即結合以上兩種繼承方式的優點,拋棄兩者的缺點,而實現的一種組合方式

function Animal(name) {
    this.name = name
    this.types = ['dog', 'cat']
}
Animal.prototype.sayName = function() {
    console.log(this.name)
}

function Dog(name, hobby) {
    // 獲取私有方法並調用父類的構造函數,並傳遞構造函數的參數,實現初始化不同的構造函數
    Animal.call(this, name)
    this.hobby = hobby
}
// 子類實例可以訪問父類prototype的方法和屬性
Dog.prototype = new Animal()
Dog.prototype.constructor = Dog
Dog.prototype.sayHobby = function() {
    console.log(this.hobby)
}

// test instance of dog1
let dog1 = new Dog('xiaohei', 'bone')
dog1.sayName() // xiaohei
dog1.sayHobby() // bone
dog1.types.push('ant') // types: ['dog', 'cat', 'ant']

// test instance of dog2
let dog2 = new Dog('feifei', 'fish')
dog2.sayName() // feifei
dog2.sayHobby() // fish
dog2.types // ['dog', 'cat']

組合模式,解決了使用構造函數繼承類式繼承帶來的問題,算是一種比較理想的解決繼承方式,但是這裏還有一些瑕疵,調用了兩次父類(Animal)的構造函數。

所以爲了解決這個問題,進行了優化,產生了👇這種繼承方式

組合寄生式繼承

function Animal(name) {
    this.name = name
    this.types = ['dog', 'cat']
}
Animal.prototype.sayName = function() {
    console.log(this.name)
}

function Dog(name, hobby) {
    // 獲取私有方法並調用父類的構造函數,並傳遞構造函數的參數,實現初始化不同的構造函數
    Animal.call(this, name)
    this.hobby = hobby
}

/**注意下面這兩行代碼**/

Dog.prototype = Object.create(Animal.prototype)
// 由於對Animal.prototype進行了淺拷貝,則改變了Dog中的構造函數,所以需要重新賦值Dog爲構造函數
Dog.prototype.constructor = Dog
Dog.prototype.sayHobby = function() {
    console.log(this.hobby)
}

// test instance of dog1
let dog1 = new Dog('xiaohei', 'bone')
dog1.sayName() // xiaohei
dog1.sayHobby() // bone
dog1.types.push('ant') // types: ['dog', 'cat', 'ant']

// test instance of dog2
let dog2 = new Dog('feifei', 'fish')
dog2.sayName() // feifei
dog2.sayHobby() // fish
dog2.types // ['dog', 'cat']
MDN解釋:Object.create()方法創建一個新對象,使用現有的對象來提供新創建的對象的__proto__。

可以理解爲:使用Object.create()進行一次淺拷貝,將父類原型上的方法拷貝後賦給Dog.prototype,這樣子類上就能擁有了父類的共有方法,而且少了一次調用父類的構造函數。

重寫create方法:

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

同時需要注意子類的constructor,由於更改了子類的prototype,所以需要重新設定子類的構造函數。

ES6中使用語法糖extends實現

如果之前有學習過,或者有面嚮對象語言基礎的,這個則很容易理解,使用extens關鍵字作爲繼承。

class Animal {
    constructor(name) {
        this.name = name
    }
    
    sayName() {
        console.log(this.name)
    }
}

class Dog extends Animal {
    constructor(name, hobby) {
        super(name)
        this.hobby = hobby
    }
    
    sayHobby() {
        console.log(this.hobby)
    }
}

let dog1 = new Dog('xiaohei', 'bone')
dog1.sayName() // xiaohei
dog1.sayHobby() // bone

let dog2 = new Dog('feifei', 'fish')
dog2.sayName() // feifei
dog2.sayHobby() // fish

總結

綜上所述,JS中的繼承總共分爲構造器繼承類式繼承組合繼承組合寄生繼承ES6中extends的繼承五種繼承方式,其中第四種是第三種的優化實現。

最後,實現new關鍵字的實現

MDN: new 運算符創建一個用戶定義的對象類型的實例或具有構造函數的內置對象的實例。

語法:new constructor[([arguments])]

function new(constructor, arguments) {
    let o = {}
    if (constructor && typeof constructor === 'function') {
        // 獲取構造函數的原形
        o.__proto__ = constructor.prototype
        // 獲取構造函數的私有變量和私有方法
        constructor.apply(o, arguments)
        return o
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章