js–如何實現繼承?

前言

  學習過 java 的同學應該都知道,常見的繼承有接口繼承和實現繼承,接口繼承只需要繼承父類的方法簽名,實現繼承則繼承父類的實際的方法,js 中主要依靠原型鏈來實現繼承,無法做接口繼承。

  學習 js 繼承之前,我們需要了解原型這一 概念,我們知道 js 中創建對象通過構造函數來創建,而每一個構造函數都有對應的 prototype 的屬性,該屬性對應的值為一個對象,這個對象也就是所有通過該構造函數創建出來的實例所共享的屬性和方法,而創建出來的每一個實例對象都有一個指針指向這些共享的屬性和方法,這個指針就是所說的 __proto__(注意這裡是雙下劃線),因此就產生了三種來獲取原型的方法,分別是 p.__proto__,p.constructor.prototype,Object.getPrototypeOf( p ),這就是我對原型的了解。

  當我們在訪問一個對象的屬性時,如果這個對象內部不存在這個屬性,那麼它就會在它的原型對象里找這個屬性,這個原型對象又會有自己的原型,於是這樣一層一層向上找下去,也就產生了原型鏈的概念。原型鏈的盡頭就是 object.prototype ,所以我們每創建的一個對象都有 toString(),valueOf() 等方法的原因。

  有了上面的基礎常識作為鋪墊,我們來看下 js 中具體怎麼來實現繼承。

正文

  js 中實現繼承的方法有6種,具體實現如下:

  (1)原型鏈實現繼承

        //定義父類
        function superFun(){
          this.superProperty = "super"//給父類構造函數添加參數屬性
        }
        superFun.prototype.getSuperValue = function(){//給父類構造函數添加原型方法
          return this.superProperty
        }
        //定義子類
        function subFun(){
          this.subProperty = "sub"
        }
        subFun.prototype = new superFun()//繼承了superFun父類 ,這一點最主要
        subFun.prototype.getSubValue = function(){//在繼承父類之後,在原型上添加新的方法或者重寫父類的方法
          return this.subProperty
        }
        var sub = new subFun()//實例化一個子類對象
        console.log(sub.superProperty);//super--判斷繼承父類的屬性
        console.log(sub.subProperty);//sub--子類的實例的屬性
        console.log(sub.getSuperValue());//super--判斷繼承父類的方法
        console.log(sub.getSubValue());//sub----子類實例的方法
        console.log(sub instanceof superFun);//true----原型鏈判斷
        console.log(sub instanceof subFun);//true----原型判斷

  上面的代碼需要注意必須在繼承父類語句之後才能在其原型上添加新的方法或者重寫父類的方法,同時添加新的方法的時候不能使用字面量的形式添加。

  所有的函數的默認原型都是 object,默認原型都會包含一個內部指針指向 object.prototype ,因此所有自定義的對象都有 toString()方法和 valueOf() 方法。

  確定原型和實例的關係的方法可以使用:instanceof 和 isPrototypeOf。

  優缺點:上面的方法讓新實例的原型等於父類的實例實現了原型鏈的繼承,子類的實例能夠繼承構造函數的屬性,構造函數的方法,父類的構造函數的屬性以及父類原型上的方法,但是新實例無法向構造函數傳參,繼承單一,所有的新實例都會共享父類構造函數的屬性,因此在父類構造函數種定義一個引用數據類型的時候,每個字類的實例都有擁有該引用類型的屬性,當其中一個實例對該屬性做了修改,別的實例也會收到影響。例子如下:

       //定義父類
        function superFun(){
          this.superProperty =  {name:"xiaoming",age:20}//給父類構造函數添加參數屬性
        }
        superFun.prototype.getSuperValue = function(){//給父類構造函數添加原型方法
          return this.superProperty
        }
        //定義字類
        function subFun(){
          this.subProperty = "sub"
        }
        subFun.prototype = new superFun()//繼承了superFun父類 ,這一點最主要
        subFun.prototype.getSubValue = function(){//在繼承父類之後,在原型上添加新的方法或者重寫父類的方法
          return this.subProperty
        }
        var sub1 = new subFun()
        var sub2 = new subFun()
        console.log(sub2.superProperty.name);//xiaoming
        sub1.superProperty.name = "xiaohong"
        console.log(sub2.superProperty.name);//xiaohong

  (2)借用構造函數實現繼承

      //定義父類
      function superFun(superProperty) {
        this.superProperty = superProperty; //給父類構造函數添加參數屬性
      }
      superFun.prototype.getSuperValue = function () {
        //給父類構造函數添加原型方法
        return this.superProperty;
      };
      function subFun() {
        superFun.call(this, "super");
        this.subProperty = "sub";
      }
      subFun.prototype.getSubValue = function () {
        return this.subProperty;
      };
      var sub = new subFun();
      console.log(sub.superProperty); //super--判斷繼承父類的屬性
      console.log(sub.subProperty); //sub--子類的實例的屬性
      //console.log(sub.getSuperValue()); //報錯sub.getSuperValue is not a function--判斷繼承父類的方法 不能繼承
      console.log(sub.getSubValue()); //sub----子類實例的方法
      console.log(sub instanceof superFun); //false----原型鏈判斷
      console.log(sub instanceof subFun); //true----原型判斷

  上面的方法借用構造函數實現繼承,主要是用 call() 或者apply() 在子類的構造函數內部調用父類的構造函數,就相當於在子類構造函數內部做了父類函數的複製並且自執行。

  優缺點:通過構造函數實現繼承,只能繼承父類構造函數的屬性,不能繼承父類原型上面的方法,無法實現構造函數的復用,每次用每次都要重新調用,相當於每個新實例都有父類構造函數的副本,造成臃腫,但是這種方法能夠解決原型鏈不能傳參的問題,對父類構造函數種屬性為引用數據類型的問題,以及通過多個 call 解決單一繼承問題等。

   (3)原型鏈和構造函數組合實現繼承(常用)

      //定義父類
      function superFun(superProperty) {
        this.superProperty = superProperty;
        this.superPropertyList = ["red", "blue", "green"];
      }
      superFun.prototype.getSuperValue = function () {
        return this.superProperty;
      };
      function subFun(property1, property2) {
        superFun.call(this, property1); //繼承屬性
        this.subProperty = property2;
      }
      subFun.prototype = new superFun(); //繼承方法
      subFun.prototype.constructor = superFun;
      //添加字類新方法
      subFun.prototype.getSubValue = function () {
        return this.subProperty;
      };
      var sub1 = new subFun("sub1Tosuper", "sub1Property");
      var sub2 = new subFun("sub2Tosuper", "sub2Property");
      console.log(sub2.superPropertyList); //["red", "blue", "green"]
      sub1.superPropertyList.push("black");
      console.log(sub2.superPropertyList); //["red", "blue", "green"]   父類引用類型數據兩者互不干擾
      console.log(sub1.superProperty); //sub1Tosuper--繼承父類的屬性
      console.log(sub1.getSuperValue()); //sub1Tosuper--繼承父類方法
      console.log(sub1.subProperty); //sub1Property--子類的屬性
      console.log(sub1.getSubValue()); //sub1Property--子類方法

  上面的代碼使用原型鏈和構造函數組合實現了繼承,其中通過原型鏈實現對原型的屬性和方法的繼承,通過借用構造函數來實現對實例屬性的繼承,這樣即保證了函數的調用,有實現了每個實例都有自己的屬性,解決了實例中屬性干擾的問題。

  優缺點:這種方法結合了前兩種模式的優點,達到了傳參和復用的效果,可以繼承父類原型的屬性和方法,可以傳參,可以復用,同時每個新實例引入的構造函數的屬性都是私有的,但是實現需要調用兩次父類構造函數,這樣就存在內存消耗問題,子類的構造函數會代替原型上的那個父類構造函數。

   (4)原型式實現繼承

      //定義父類
      function superFun(superProperty) {
        this.superProperty = superProperty;
      }
      superFun.prototype.getSuperValue = function () {
        return this.superProperty;
      };
      function subFun(obj) {
        function F() {}
        F.prototype = obj;
        return new F();
      }
      var super1 = new superFun("super1");
      var sub = subFun(super1);
      console.log(sub.superProperty);//super1
      console.log(sub.getSuperValue());//super1

  上面的代碼重點在於在 subFun() 函數內部創建一個臨時性的構造函數,然後將傳入的對象作為這個構造函數的原型,最後返回這個臨時類型的一個新實例,相當於用一個函數包裝了一個對象,然後返回這個函數的的調用,這個函數會就編程了可以隨意增添屬性的實例或者對象, object.create() 就是這個原理。es5中object.create() 接受兩個參數,一個參數作為新對象原型的對象,另一個可選參數作為新對象定義額外屬性的對象,當兩個參數都存在的時候,任何屬性都會覆蓋原型對象上的同名屬性。

  優缺點:這種方法類似於複製一個對象,用函數來包裝,其實就是哪一個對象作為繼承,然後傳入另一個對象,本質就是對傳入的對象進行一次淺拷貝,但是所有實例都會繼承原型上的屬性,且無法實現復用,若包含引用數據類型始終會共享相應的值。

   (5)寄生式實現繼承

      //定義父類
      function superFun(superProperty) {
        this.superProperty = superProperty;
      }
      superFun.prototype.getSuperValue = function () {
        return this.superProperty;
      };
      function subFun(obj) {
        function F() {}
        F.prototype = obj;
        return new F();
      }
      var super1 = new superFun("super1");
      function subObject(obj){
        var sub=subFun(obj)
        sub.subPorperty="subPorperty"
        return sub
      }
      var sub = subObject(super1);
      console.log(sub.superProperty); //super1
      console.log(sub.subPorperty);//subPorperty
      console.log(sub.getSuperValue()); //super1

  上面的代碼對比原型式繼承,其實就是在原型式繼承的基礎上套了一層殼子,創建了一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最後再返回一個對象。

  優缺點:這種方法沒有創建自定義類型,因為只是給返回的對象添加了一層殼子,實現了創建的新對象,但是這種方法沒有用到原型,無法實現復用。

  (5)寄生組合實現實現繼承(常用)

  針對組合實現繼承存在的問題進行了優化,前面說到組合繼承要調用兩次父類構造函數,第一次是在創建子類原型的時候,第二次是在子類構造函數內部 call 調用。對於這兩次調用,第一次調用父類是可以避免的,不必為了指定子類型的原型而調用夫類型的構造函數,我們無非是需要一個父類型原型的一個副本而已。

      //定義父類屬性
      function superFun(superProperty) {
        this.superProperty = superProperty;
        this.superPropertyList = ["red", "blue", "green"];
      }
      //定義父類原型上的方法
      superFun.prototype.getSuperValue = function () {
        return this.superProperty;
      };
      //使用寄生
      function object(obj) {
        function F() {}
        F.prototype = obj;
        return new F();
      }
      function inheritObject(subFun, superFun) {
        var _prototype = object(superFun.prototype); //創建對象
        _prototype.constructor = subFun; //增強對象
        subFun.prototype = _prototype; //指定對象
      }
      //使用組合
      function subFun(tosuperProperty,subProperty){
        superFun.call(this,tosuperProperty)
        this.subProperty=subProperty
      }
      //子類繼承父類
      inheritObject(subFun,superFun)
      //子類原型的方法
      subFun.prototype.getSubValue=function(){
        return this.subProperty
      }
      var sub=new subFun("super","sub")
      console.log(sub.superProperty);//super
      console.log(sub.subProperty);//sub
      console.log(sub.getSuperValue());//super
      console.log(sub.getSubValue());//sub
      console.log(sub instanceof superFun);//true
      console.log(sub instanceof subFun);//true

  上面的方法是 js 中實現繼承最常見方法,它完美解決了組合式繼承的中兩次調用父類原型的bug,通過寄生,在函數內部返回對象然後調用,使用組合,使得函數的原型等於另一個實例,在函數中調用 call 引入另一個構造函數,實現了可以傳參的功能,避免了在父類原型上創建不必要的屬性,成為最理想的實現繼承的方法。需要注意  inheritObject() 函數接受兩個參數,分別式子類和父類的兩個構造函數。

  優缺點:使用寄生式繼承實現了繼承父類的原型,然後再將結果指定給子類型的原型。使用組合繼承得到傳參復用等效果。

總結

  以上就是本文的全部內容,希望給讀者帶來些許的幫助和進步,方便的話點個關注,小白的成長之路會持續更新一些工作中常見的問題和技術點。