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

carbrokers發表於2019-02-28

1.實現new函式

在JS中初始化一個例項的時候,會呼叫new去完成例項化,那麼new函式到底幹了些什麼事情,

  • 例項可以訪問建構函式中的物件
  • 例項可以訪問建構函式prototype中的內容

此外,我們都知道在chrome,firefox等瀏覽器中,例項化的物件一般都通過 __proto__指向建構函式的原型,所以會有一條如下的對應關係

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

所以我們可以實現最簡單的new的山寨函式

function mockNew() {
     var obj = new Object()
     //獲取建構函式
     var Constructor = [].shift.call(arguments)
     //繫結原形鏈
     obj.__proto__ = Constructor.protoype
     //呼叫建構函式
     Constructor.apply(obj,arguments)
     return obj
}
複製程式碼

通過以上方法,我們就山寨了一個最簡單的new方法,這個山寨的new方法可以更好的幫我們去理解繼承的全部過程。

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

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

  • 無法向建構函式傳入引數
  • 引用型別的資料會被例項共享

第一個原因很清楚也很容易解決,那麼為什麼會專門對引用型別產生問題呢,還是先上程式碼

//父類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方法回顧整個過程

//例項話父類,繫結到子類的原形鏈上
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 )

緊接著我們在例項化子型別例項

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更瞭解了!

相關文章