JavaScript 學習之繼承

Javascript 的繼承的實現方法有不少種,以前雖然學習過,可是沒有綜合整理過,這一次就來整理整理 Javascript 語言的繼承方面的知識。關於詳細的Javascript 的繼承方面的知識,推薦你們去看那本紅寶書 ————《JavaScript高級程序設計》。javascript

雖然 ES6 推出了 class 這個概念,方便了咱們開發人員的學習和理解,可是,class 只是一個語法糖,實際上底層的實現仍是原來的那一套,利用原型鏈和構造函數來實現繼承。所以要想 Javascript 的基本功牢實一點,仍是須要去學習這些知識的。java

在 Javascript 的繼承實現裏,目前有原型鏈繼承法,構造函數繼承法,組合繼承法等等方法,下面我就一一對這些方法來進行說明。app

1. 原型鏈繼承

原型鏈繼承法是運用 Javascript 的原型來實現,在 Javascript 中任意函數都擁有 prototype__proto__ 這兩個屬性,而每一個對象都擁有一個 __proto__ 屬性,對象裏 __proto__ 屬性的值是來自於構造這個對象的函數的 prototype 屬性,經過 prototype__proto__ ,咱們構造出原型鏈,而後利用原型鏈來實現繼承。函數

具體的代碼例子以下學習

function Animal() {
    this.type = 'Cat'
    this.name = 'Nini'
    this.hobbies = ['eat fish', 'play ball']
}
Animal.prototype.say = function () {
    console.log('type is ' + this.type + ' name is ' + this.name);
}

function Cat() {
    this.age = '1'
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat

let smallCat = new Cat()
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is Cat name is Nini

let bigCat = new Cat()
console.log(bigCat.hobbies) // [ 'eat fish', 'play ball', 'sleep' ]
複製代碼

從上面的例子咱們能夠看到,原型鏈繼承的優勢:優化

  • 多個實例共同引用可複用的屬性和方法,不是建立每個實例的時候再建立一遍這些數據

缺點:ui

  • 全部的屬性都被實例所共享,這意味着若是屬性是基本數據類型的話,實例是沒法修改這個屬性的值,由於實例會新增一個同名的屬性,咱們只能對新增的屬性進行操做,以剛剛的代碼爲例
smallCat.name = 'Kiki' // 此時 smallCat 對象上新增了 name 屬性,若是訪問這個屬性的話,咱們獲得是這個新增的屬性而不是在原型上的 name 屬性
console.log(smallCat.name) // 'Kiki'
console.log(bigCat.name) // 'Nini'
複製代碼

若是屬性是引用屬性的話,修改這個屬性所指向的數據裏的內容將會影響全部的實例(注意不是對屬性直接賦值,若是直接賦值了就像基本數據類型同樣,在實例自己上新建一個同名屬性),如以前的代碼實例this

smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies) // [ 'eat fish', 'play ball', 'sleep' ]
複製代碼

2. 構造函數繼承

構造函數繼承的基本原理就是利用 call, apply 這樣的能夠指定函數 this 值的方法,來實現子類對父類屬性的繼承,例子以下spa

function Animal(type, name) {
    this.type = type
    this.name = name
    this.hobbies = ['eat fish', 'play ball']
}
function Cat(type, name) {
    Animal.call(this, type, name)
    this.age = '1'
    this.say = () => {
        console.log('type is ' + this.type + ' name is ' + this.name);
    }
}
let smallCat = new Cat('Cat', 'Nini')
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is Cat name is Nini

let bigCat = new Cat('Cat', 'Nicole')
console.log(bigCat.hobbies) // [ 'eat fish', 'play ball' ]
bigCat.say() // type is Cat name is Nicole
複製代碼

從上面的例子能夠看到,構造函數繼承的優勢是prototype

  • 全部的實例沒有共享引用屬性,也就是說每一個實例都獨立擁有一份從父類那裏繼承來的屬性,任一個實例修改了引用屬性裏的數據內容,並不會影響到其餘的實例

  • 可向父函數傳參

缺點:

  • 因爲全部的屬性和方法都再也不被全部的實例共享,所以那些公有的屬性和方法就會被重複的建立,形成了內存的額外開銷

3. 組合繼承 (原型鏈繼承和構造函數繼承的合體)

其實經過以前的分析,能夠知道,不管是原型鏈繼承仍是構造函數繼承,都存在本身的優缺點,對於咱們的開發實現而言,都是不完美的。原型鏈繼承把全部的屬性和方法都共享給了全部的實例,也就是說,咱們想要個性化的針對某一實例上所繼承的引用屬性的數據內容進行修改的話,這一操做將同時影響別的實例,這可能會給咱們的開發帶來必定的問題。構造函數繼承把全部的屬性和方法都爲每一個實例單獨拷貝了一份,雖然實現了實例之間的數據隔離,可是對於那些原本就應該是公共的屬性和方法來講,重複而無心義的複製也無疑是增長了額外的內存開銷。

所以,組合繼承方法吸取了這兩個方法的優勢,同時避免了各自的缺點,是一種可行的實現繼承的方法,實現的代碼以下

function Animal(type, name) {
    this.type = type
    this.name = name
    this.hobbies = ['eat fish', 'play ball']
}
Animal.prototype.say = function () {
    console.log('type is ' + this.type + ' name is ' + this.name);
}
function Cat(type, name) {
    Animal.call(this, type, name) // 構造函數繼承
    this.age = '1'
}
Cat.prototype = new Animal() // 原型鏈繼承
Cat.prototype.constructor = Cat

let smallCat = new Cat('smallCat', 'Nini')
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is smallCat name is Nini

let bigCat = new Cat('bigCat', 'Nicole')
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball' ]
bigCat.say() // type is bigCat name is Nicole
複製代碼

組合繼承方法的思路是將公共的屬性和方法放在父類的 prototype 上,而後利用原型鏈繼承來實現公共的屬性和方法的繼承,而對於那種每一個實例均可自定義修改的屬性採起構造函數繼承的方法來實現每一個實例都獨有一份這樣的屬性。

4. 原型式繼承

原型式繼承的實現原理就是將一個對象做爲建立對象的原型傳入到一個構建新對象的函數中,好比

function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}
複製代碼

其實原型式繼承的思路也就是 Object.create() 方法的實現思路,來看看一個完整的原型式繼承的實現,代碼以下

let Animal = {
    type: 'Cat',
    name: 'Nini',
    hobbies: ['eat fish', 'play ball']
}

function createCat(o) {
    function F() {}
    F.prototype = o
    return new F()
}

let smallCat = createCat(Animal)
let bigCat = createCat(Animal)
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
bigCat.name = 'Nicole' // 直接在 bigCat 這個對象上新增一個 name 屬性,並不是去修改原型上的 name 屬性
console.log(smallCat.name); // 'Nini'
console.log(bigCat.name); // 'Nicole'
console.log(bigCat.__proto__.name); // 'Nini' 原型上的 name 屬性依舊保持
複製代碼

原型式繼承法其實和原型鏈繼承有點類似,都是全部的屬性和方法放在了原型上,若是建立全部的實例時都用的是同一個對象做爲原型的話,那麼原型鏈繼承遇到的問題,這個方法一樣也有。

關於原型式繼承的更多思考

在學習原型式繼承的時候,我想到了若是建立每一個實例的時候,傳入的父類對象都是不一樣的對象,可是都是同屬於一個父類的對象,那麼若是咱們將公共的屬性和方法放在父類的原型上,把可自定義的屬性放在父類的構造函數上,那也能夠實現比較合理的繼承,具體代碼以下

function Animal(type, name) {
    this.type = type
    this.name = name
    this.hobbies = ['eat fish', 'play ball']
}
Animal.prototype.say = function () {
    console.log('type is ' + this.type + ' name is ' + this.name);
}
function createCat(o) {
    function F() {}
    F.prototype = o
    return new F()
}

let smallCat = createCat(new Animal('smallCat', 'Nini'))
let bigCat = createCat(new Animal('bigCat', 'Nicole'))
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball' ]
複製代碼

這個思路看起來不錯,可是仔細想一想仍是有必定的問題的,相比於以前提到的組合式繼承來講,這個方法每次在建立實例的時候,咱們都會 new 一個新的父類實例,這其實形成了內存的浪費,而組合繼承則保證了父類的實例只會被 new 一次,而那些能夠自定義的屬性都被存在每一個子類的實例中,保證了數據的互不影響,咱們能夠經過下面的圖片來看看具體的差別

5.寄生式繼承

寄生式繼承其實和原型式繼承的實現有些類似,不過寄生式繼承在原型式繼承的基礎上添加了在建立實例的函數中以某種形式來加強對象,最後返回對象。其實意思就是,在建立子實例的函數中,先經過原型式繼承的方法建立一個實例,而後爲這個實例添加屬性和方法,最後返回這個實例,代碼實例以下

function createCat(o) {
    let cloneObj = Object.create(o)
    cloneObj.say = function (){ // 爲實例添加一個 say 方法
        console.log('type is ' + this.type + ' name is ' + this.name);
    }
    return cloneObj
}

let Animal = {
    type: 'Cat',
    name: 'Nini',
    hobbies: ['eat fish', 'play ball']
}

let smallCat = createCat(Animal)
let bigCat = createCat(Animal)
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is Cat name is Nini
bigCat.say() // type is Cat name is Nini
複製代碼

經過上面代碼咱們能夠很清楚的看到,寄生式繼承有原型鏈繼承的缺點和構造函數繼承的缺點,也就是說經過寄生式繼承創造出來的實例,若是修改了它原型上的引用屬性裏的內容,其餘的實例也會受影響,並且每次建立實例的時候,那些公共的屬性和方法都會被建立一次。

6. 寄生組合式繼承

上面咱們提到了組合式繼承是一種還不錯的繼承實現方式,既能讓每一個實例擁有繼承來的可自定義的屬性和方法,也能共享公共的方法和屬性。可是這種方法還有可以優化的地方,這個須要優化的點在於,組合式繼承時,父類的構造函數會被調用兩次,結合代碼看一下

function Cat(type, name) {
    Animal.call(this, type, name) // 這裏調用了一次父類的構造函數
    this.age = '1'
}
Cat.prototype = new Animal() // 這裏也調用了一次父類的構造函數
Cat.prototype.constructor = Cat
複製代碼

實際上,子函數的 prototype 只須要指向那些公共的屬性和方法就能夠了,不須要指向整個父函數的實例,因爲咱們把須要繼承的公共的屬性和方法放在了父函數prototype 上,因此咱們能夠考慮讓子函數的 prototype 間接訪問父函數的 prototype。實現的代碼例子以下

// 利用寄生式繼承來讓子函數的 prototype 能訪問到父函數的原型
function createObj(child, parent) {
    let prototype = Object.create(parent.prototype) 
    // 這個對象相比於父實例少了那些子函數已經過parent.call 繼承到的屬性和方法,僅僅含有一個指向父函數原型的屬性
    prototype.constructor = child
    child.prototype = prototype
}
createObj(Cat, Animal)
複製代碼

最後,完整的寄生組合式繼承的實現代碼以下

function Animal(type, name) {
    this.type = type
    this.name = name
    this.hobbies = ['eat fish', 'play ball']
}
Animal.prototype.say = function () {
    console.log('type is ' + this.type + ' name is ' + this.name);
}
function Cat(type, name) {
    Animal.call(this, type, name)
    this.age = '1'
}

function createObj(child, parent) {
    let prototype = Object.create(parent.prototype)
    prototype.constructor = child
    child.prototype = prototype
}
createObj(Cat, Animal)

let smallCat = new Cat('smallCat', 'Nini')
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is smallCat name is Nini

let bigCat = new Cat('bigCat', 'Nicole')
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball' ]
bigCat.say() // type is bigCat name is Nicole
複製代碼

所以寄生組合式繼承在吸收了組合式繼承的優勢上,避免了在子函數的原型上面建立沒必要要的、多餘的屬性,而寄生組合式繼承也是目前的一種理想的比較好的繼承方法的實現。

總結

其實 Javascript 繼承的關鍵點是必定要將私有的屬性和方法,公有的屬性和方法分別處理,私有的屬性和方法須要讓每一個實例都獨有一份,保證數據的更改互不影響,公有的屬性和方法須要放在父類的原型上,確保不重複建立。

相關文章
相關標籤/搜索