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
複製程式碼
程式碼優化
在子類中,既然是需要獲取父類的私有屬性,則可以使用call
和apply
,當呼叫父類的方法的時候,改變當前上下文為子類物件,則子類物件就可以獲取到了父類的所有私有屬性。
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
的實現。
類式繼承會有兩方面的缺點
-
引用陷阱-子類物件可以隨意修改父類中的方法和變數,並影響其他子類物件
dog1.types.push('fish') console.log(dog1.types) // ["cat", "dog", "fish"] console.log(dog2.types) // ["cat", "dog", "fish"] 複製程式碼
-
無法初始化構造不同的例項屬性
這個主要是由於類式繼承,是通過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
}
}複製程式碼