JS中繼承的實現

Swiftly發表於2019-01-14

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
    }
}複製程式碼

相關文章