簡單易懂的JS繼承圖解
JS繼承的實現方式一共有八種。下面我們來一個一個瞅一下。注意⚠️:文章依據個人理解,難免有問題,還望各位及時指出!!!!!
- 原型鏈繼承
- 借用構造函數繼承
- 組合繼承
- 原型式繼承
- 寄生繼承
- 寄生組合式繼承
- 原型拷貝和構造函數實現繼承
- Class繼承
- 混入方式繼承多個對象
我們先創建一個父類
// 父類 function Animal(name, color){ this.name = name; this.attribute = { color: color, } this.action = function (currentAction) { console.log(this.name + currentAction) } }
原型鏈繼承
實現
原理:將父類的實例作為子類的原型
function OrangeCat(){}; OrangeCat.prototype = new Animal('橘貓','橘色🍊'); // 相當於OrangeCat.prototype.__proto__ = new Animal('橘貓','橘色🍊').__proto__; // __proto__是系統變數,可以省略 let firstOrangeCat = new OrangeCat();
缺陷
- 缺少constructor,需要手動添加
- 引用類型的屬性被所有子類實例共享
- 子類實例化時無法向父類構造函數傳參
缺少constructor
我們直接列印一下OrangeCat,會發現缺少constructor,我們可以使用OrangeCat.prototype.constructor手動添加上constructor
引用類型的屬性被所有子類實例共享
讓我們來看一下下面的例子🌰
function OrangeCat(){} OrangeCat.prototype = new Animal('橘貓','橘色🍊'); // 第一隻橘貓 let firstOrangeCat = new OrangeCat(); // 第二隻橘貓 let secondOrangeCat = new OrangeCat(); console.log('第一隻橘貓的顏色:' + firstOrangeCat.attribute.color); console.log('第二隻橘貓的顏色:' + secondOrangeCat.attribute.color); // 將第一隻橘貓的顏色改為黑色 firstOrangeCat.attribute.color = 'black'; console.log('顏色改變後第一隻橘貓的顏色:' + firstOrangeCat.attribute.color); console.log('顏色改變後第二隻橘貓的顏色:' + secondOrangeCat.attribute.color);
結果:
圖解
借用構造函數繼承
實現
原理: 使用父類的構造函數來增強子類實例,等同於複製父類的實例給子類(不使用原型),可以實現多繼承(call多個父類對象)
function YellowDog(name, color) { Animal.call(this, name, color); } let firstYellowDog = new YellowDog('狗', '黃');
缺陷
- 只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法
console.log(firstYellowDog instanceof Animal); // false console.log(firstYellowDog instanceof YellowDog); // true
- 無法實現復用,每個子類都有父類實例函數的副本,影響性能
圖解
新創建一個BlackDog子類
function BlackDog(name, color) { Animal.call(this, name, color); }
組合繼承
實現
原理:組合原型鏈繼承和借用構造函數繼承,用原型鏈實現對原型屬性和方法的繼承,用借用構造函數技術來實現實例屬性的繼承。
解決了原型鏈繼承中父類引用類型的屬性被所有子類實例共享問題以及借用構造函數繼承中只能繼承父類的實例屬性和方法卻不能繼承原型屬性/方法的問題,使子類實例共享引用對象子類實例既不共享父類的引用類型的數據,也繼承了原型。
如何解決父類引用類型的屬性被所有子類實例共享問題?
因為構造函數會將屬性附加到子類實例上,訪問屬性的時候直接會訪問子類實例上的屬性,相當於子類實例上的屬性直接屏蔽了原型上的屬性,避免了共享個問題的出現
function Pig(name, color) { Animal.call(this, name, color); } Pig.prototype = new Animal(); Pig.prototype.constructor = Pig; let firstPig = new Pig('豬', '白');
缺陷
- 由於調用了兩次Animal,會導致有重複屬性
console.log(firstPig)
- 每個子類都有父類實例函數的副本,影響性能
圖解
原型式繼承
實現
利用一個空對象作為中介,將某個對象直接賦值給空對象構造函數的原型。
實現1:
let cattle = { name:'牛', attribute: { color: '黃', } } let firstCattle = Object.create(cattle);
實現2:
function object(obj){ function F(){}; F.prototype = obj; return new F(); } let cattle = { name:'牛', attribute: { color: '黃', } } let firstCattle = object(cattle);
缺陷
- 引用類型的屬性被實例共享
let secondCattle = object(cattle); console.log(firstCattle.attribute); // 黃 console.log(secondCattle.attribute); // 黃 firstCattle.attribute.color = '紅'; console.log(firstCattle.attribute); // 紅 console.log(secondCattle.attribute); // 紅
- 子類實例化時無法傳參
圖解
寄生繼承
實現
在原型式繼承的基礎上,增強對象,返回構造函數。
let sheep = { name: '羊', action: (currrentAction)=>{ console.log(currrentAction) } } function createSheep(params) { let clone = object(params);// 此處的object就是上文中原型式繼承的object方法 clone.say = ()=>{ console.log('咩咩咩'); } return clone; } let anSheep = createSheep(sheep);
缺陷
- 引用類型的屬性被實例共享(可參考原型式繼承)
- 子類實例化時無法傳參
圖解
寄生組合式繼承
實現
結合借用構造函數傳遞參數和寄生模式實現繼承。
只調用了一次Animal構造函數,因此避免了在Chicken.prototype
上創建不必要的、多餘的屬性。與此同時,原型鏈還能保持不變;因此,還能夠正常使用instanceof
和isPrototypeOf()
。這是最成熟的方法,也是現在庫實現的方法
function Chicken(name, color){ // 借用構造函數傳遞增強子類實例屬性(支援傳參和避免篡改) Animal.call(this, name); } // 將父類原型指向子類 let clonePrototype = Object.create(Animal.prototype); // 創建對象,創建父類原型的一個副 clonePrototype.constructor = Chicken;// 增強對象,彌補因重寫原型而失去的默認的constructor Chicken.prototype = clonePrototype; // 將新創建的對象賦值給子類的原型 let firstChicken = new Chicken("雞", "烏");
缺陷
- 每個子類都有父類實例函數的副本,影響性能
圖解
原型拷貝和構造函數實現繼承
實現
結合借用構造函數傳遞參數和遍歷父類的原型鏈循環賦值給子類原型鏈來實現繼承。和組合繼承以及寄生組合式繼承一樣會調用Amimal.call(),不同對是三者對原型鏈的處理方式不同
function Fish(name, color){ Animal.call(this, name, color) } for(var key in Animal.prototype) { Fish.prototype[key] = Animal.prototype[key] } Fish.prototype.constructor = Fish; let firstFish = new Fish('魚', '紅');
缺陷
- 不可遍歷的屬性不會被繼承
圖解
Class繼承
實現
ES6提供的繼承方式,其extends的實現和上述的寄生組合式繼承方式一樣.
class Rabbit { constructor(name) { this.name = name; } action(currentAction){ console.log(`當前動作${currentAction}`) } } class FirstRabbit extends Rabbit{ constructor(name){ super('兔子'); } ownName(){ } } let firstRabbit = new FirstRabbit('小白兔') console.log(firstRabbit)
我們來看下結果
我們可以看到class繼承也是通過原型鏈實現的,實際上ES6的class只是一個語法糖🍬。
混入方式繼承多個對象
實現
通過借用構造函數繼承和Object.assign()實現多繼承。在寄生組合的基礎上再進一步。
// 混入方式實現多繼承 function OthenClass(){} function Tiger(){ Animal.call(this); OthenClass.call(this); } // 繼承一個類 Tiger.prototype = Object.create(Animal.prototype); // 混合其它 Object.assign(Animal.prototype, OthenClass.prototype); // 重新指定constructor MyClass.prototype.constructor = MyClass;
問題⚠️
函數聲明和類聲明的區別
函數聲明會提升,類聲明不會。首先需要聲明你的類,然後訪問它,否則會拋出一個ReferenceError。
ES5繼承和ES6繼承的區別
- ES5的繼承實質上是先創建子類的實例對象,然後再將父類的方法添加到this上(Parent.call(this)).
- ES6的繼承有所不同,實質上是先創建父類的實例對象this,然後再用子類的構造函數修改this。因為子類沒有自己的this對象,所以必須先調用父類的super()方法,否則新建實例報錯。
特別注意⚠️:
基於原型鏈實現的繼承都存在引用類型的屬性共享的問題,文中所講的的不共享引用類型的屬性僅指不共享父類引用類型的屬性
參考
JS高級程式設計