JS繼承,中間到底幹了些什麼

1.實現new函數

在JS中初始化一個實例的時候,會調用new去完成實例化,那麼new函數到底幹了些什麼事情,javascript

  • 實例能夠訪問構造函數中的對象
  • 實例能夠訪問構造函數prototype中的內容

此外,咱們都知道在chrome,firefox等瀏覽器中,實例化的對象通常都經過 __proto__指向構造函數的原型,因此會有一條以下的對應關係java

function Person () {}
var p = new Person()
p.__proto__ = Person.prototype
複製代碼

因此咱們能夠實現最簡單的new的山寨函數chrome

function mockNew() {
     var obj = new Object()
     //獲取構造函數
     var Constructor = [].shift.call(arguments)
     //綁定原形鏈
     obj.__proto__ = Constructor.protoype
     //調用構造函數
     Constructor.apply(obj,arguments)
     return obj
}
複製代碼

經過以上方法,咱們就山寨了一個最簡單的new方法,這個山寨的new方法能夠更好的幫咱們去理解繼承的所有過程。數組

2.原形鏈繼承過程以及缺點解析

首先咱們都知道原形鏈繼承會存在必定的問題,紅寶書上說的很清楚,這種繼承方式會產生兩個問題瀏覽器

  • 沒法向構造函數傳入參數
  • 引用類型的數據會被實例共享

第一個緣由很清楚也很容易解決,那麼爲何會專門對引用類型產生問題呢,仍是先上代碼bash

//父類Animal
function Animal() {
    this.name = 'animal'
    this.food = ['food']
}

Animal.prototype = {
    constructor: Animal,
    //更改name
    setName: function(name) {
        this.name = name
    },
    //更改food
    giveFood: function(food) {
        this.food.push(food)
    }
}

//子類AnimalChild
function AnimalChild() {}
//綁定原形鏈
AnimalChild.prototype = new Animal()

let cat = new AnimalChild()
let dog = new AnimalChild()
cat.setName('cat')
cat.giveFood('fish')

console.log(cat.name)  //cat 輸出cat很正常
console.log(dog.name)  //animal 這個就有點神奇了

console.log(cat.food)  // ['food','fish']
console.log(dog.food)  // ['food','fish']
複製代碼

經過以上的例子,咱們發現原形式繼承並非全部的數據都會共享,產生影響的數據只有引用類型的,這個的緣由是爲何呢,咱們來使用咱們的山寨new方法回顧整個過程app

//實例話父類,綁定到子類的原形鏈上
AnimalChild.prototype = mockNew(Animal)
複製代碼

當咱們這麼調用的時候,回顧一下mocknew的執行過程,其中會執行這樣一步函數

//在構造的過程當中,子類會調用父類構造函數
Animal.apply(obj,arguments)
複製代碼

這步的執行,會致使自己在父類構造函數中的this.name被綁定到了一個新的函數上,由於最終的返回值被複制到子類型的protoype上,因此,子類的protoye長得是如下模樣優化

//打印子類的prototype
console.log(AnimalChild.prototype)

//打印結果
AnimalChild.prototype = {
    name: 'animal',
    food: ['food'],
    __proto__: {
        constructor: Animal,
        setName: function() {},
        giveFood: function() {}
    }
}
複製代碼

能夠看出藉由new方法,父類構造函數中的變量綁在在子類的原形上(prototype),而父類的原形綁在了子類原形的原形實例上(prototype.proto )ui

緊接着咱們在實例化子類型實例

var cat = mockNew(animalType)
複製代碼

這步咱們會修改cat對象的__proto__屬性,最終生成的cat實例打印以下

console.log(cat)

//打印的結果
cat = {
    __proto__: {
        name: 'animal',
        food: ['food'],
        __proto__: {
            constructor: Animal,
            setName: function() {},
            giveFood: function() {}
        }
    }
}
複製代碼

能夠看出全部父類型的變量都被綁定在了實例的原形上,那爲何引用類型的數據類型會產生錯誤呢,這其實和引用類型的修改方式有關

當咱們修改name的時候,函數會主動在對象自己去賦值,及

cat.name = 'cat'
console.log(cat)

//打印結果
cat = {
    //綁定在對象上,而不是原形鏈
    name: cat
    __proto__: {
        name: 'animal',
        food: ['food'],
        //.....
    }
}
複製代碼

而當咱們對引用類型的數組進行操做的時候,函數會優先找函數自己時候有這個變量,若是沒有的話,回去原形鏈上找

cat.name.push('fish')
console.log(cat)

//打印結果
cat = {
    __proto__: {
        name: 'animal',
        //在原形鏈上修改
        food: ['food''fish'],
        //.....
    }
}
複製代碼

3.寄生組合式繼承

雖說原形式繼承會帶來問題,可是實現的思路是很是有用的,對於父類的方法,變量,通通放在原形鏈上,繼承的時候,將同名的內容通通覆蓋,放在對象自己,這樣就解決了函數的繼承和內容的重寫

基於此,寄生組合的方法獲得重視,下面分析如下執行過程,依然是使用上面的Animal和AnimalChild類

//寄生組合式方法調用

//聲明父類
function Animal() {
    this.name = 'animal'
    this.food = ['food']
}

Animal.prototype = {
    constructor: Animal,
    //更改name
    setName: function(name) {
        this.name = name
    },
    //更改food
    giveFood: function(food) {
        this.food.push(food)
    }
}

//開始繼承
function AnimalChild() {
    Animal.call(this)
}

function f() {}
f.prototype = Animal.prototype
var prototype = new f()

prototype.constructor = AnimalChild
AnimalChild.prototype = prototype

複製代碼

上述代碼和原形式繼承主要有兩點區別

  • 在子類型中使用call調用父類
  • 經過new一個空函數交換prototype

首先說一下第一點,調用call,調用call以後,至關於在子類的構造函數內部執行了一變父類的構造函數,這個時候,父函數內部經過this聲明得一些屬性都轉嫁到了子函數的構造函數中,這樣就解決了原形式繼承中變量共享的問題

其次,下面的prototype賦值方法帶有一點優化的屬性,由於父類構造函數中的內容經過call已經所有拿到了,只須要再將原形綁定就能夠了,此外,經過new的方式,子類的掛在原形鏈上的方法其實是和父類原形方法跨層級的

//爲子類添加原形方法
AnimalChild.prototype.childMethod = function() {}
console.log(AnimalChild.prototype)

//打印結果
AnimalChild.prototype = {
    construcor: AnimalChild,
    //綁定在原形上
    childMethod: function() {},
    
    //父類的原形方法都在這裏
    __proto__: {
        setName: function() {},
        giveFood: function() {}
    }
}
複製代碼

ES6中的super

經過寄生組合式繼承咱們能夠獲得以下結論,加入B繼承了A,那麼能夠獲得一個等式

B.prototype.__proto__ = A.prototype
複製代碼

知足這個等式的話其實咱們就能夠說B繼承了A的原形連接

在ES6中的super效果下,其實實現了兩條等式

B.__proto__ = A

//原形鏈相等
B.prototype.__proto__ = A.prototype
複製代碼

第二條等式咱們理解,那麼第一條等式是什麼意思呢,在寄生組合式繼承中,咱們使用call的方式去調用父類構造函數,而在ES6中,咱們能夠理解爲 子類的構造函數是基於父類實例的加工,super返回的是一個父類的實例,這樣也就解釋了等式一之間的關係。

固然,ES6中的實現方法更爲優雅,藉由一個ES6中提供的Api:setPrototypeOf,能夠用以下方式實現

class A {}

class B {}

//原形繼承
Object.setPrototypeOf(B.prototype, A.prototype);
//構造函數繼承
Object.setPrototypeOf(B, A);
複製代碼

結語

仔細的總結了如下以後,發現對於JS更瞭解了!

相關文章
相關標籤/搜索