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