JS從誕生之初本就不是面向對象的語言。app
如何在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
複製代碼
代碼優化this
在子類中,既然是須要獲取父類的私有屬性,則可使用call
和apply
,當調用父類的方法的時候,改變當前上下文爲子類對象,則子類對象就能夠獲取到了父類的全部私有屬性。spa
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
的實現。prototype
類式繼承會有兩方面的缺點code
引用陷阱-子類對象能夠隨意修改父類中的方法和變量,並影響其餘子類對象對象
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,因此須要從新設定子類的構造函數。
若是以前有學習過,或者有面向對象語言基礎的,這個則很容易理解,使用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
}
}複製代碼