簡單易懂的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

image.png

引用類型的屬性被所有子類實例共享

讓我們來看一下下面的例子🌰

    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);

結果:

image.png

圖解

image.png

借用構造函數繼承

實現

原理: 使用父類的構造函數來增強子類實例,等同於複製父類的實例給子類(不使用原型),可以實現多繼承(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);
   }

 

image.png

組合繼承

實現

原理:組合原型鏈繼承和借用構造函數繼承,用原型鏈實現對原型屬性和方法的繼承,用借用構造函數技術來實現實例屬性的繼承。

解決了原型鏈繼承中父類引用類型的屬性被所有子類實例共享問題以及借用構造函數繼承中只能繼承父類的實例屬性和方法卻不能繼承原型屬性/方法的問題,使子類實例共享引用對象子類實例既不共享父類的引用類型的數據,也繼承了原型。

如何解決父類引用類型的屬性被所有子類實例共享問題?

因爲構造函數會將屬性附加到子類實例上,訪問屬性的時候直接會訪問子類實例上的屬性,相當於子類實例上的屬性直接屏蔽了原型上的屬性,避免了共享個問題的出現

  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)

image.png

  • 每個子類都有父類實例函數的副本,影響性能

圖解

image.png

原型式繼承

實現

利用一個空對象作爲中介,將某個對象直接賦值給空對象構造函數的原型。

實現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); //
  • 子類實例化時無法傳參

圖解

image.png

寄生繼承

實現

在原型式繼承的基礎上,增強對象,返回構造函數。

  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);

缺陷

  • 引用類型的屬性被實例共享(可參考原型式繼承)
  • 子類實例化時無法傳參

圖解

image.png

寄生組合式繼承

實現

結合借用構造函數傳遞參數和寄生模式實現繼承。

只調用了一次Animal構造函數,因此避免了在Chicken.prototype 上創建不必要的、多餘的屬性。與此同時,原型鏈還能保持不變;因此,還能夠正常使用instanceofisPrototypeOf()。這是最成熟的方法,也是現在庫實現的方法

  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("雞", "烏");

缺陷

  • 每個子類都有父類實例函數的副本,影響性能

圖解

image.png

原型拷貝和構造函數實現繼承

實現

結合借用構造函數傳遞參數和遍歷父類的原型鏈循環賦值給子類原型鏈來實現繼承。和組合繼承以及寄生組合式繼承一樣會調用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('魚', '紅');

缺陷

  • 不可遍歷的屬性不會被繼承

圖解

image.png

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)

我們來看下結果

image.png

我們可以看到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高級程序設計

 卑微前端在線求關注,覺得代碼塊不顯眼的話可以去我的語雀文檔查看https://www.yuque.com/suihangadam/powcpt/ukt8a4

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章