js--如何實現繼承?

丶Serendipity丶發表於2021-04-06

前言

  學習過 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() 函式接受兩個引數,分別式子類和父類的兩個建構函式。

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

總結

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

 

相關文章