JavaScript之深刻各類繼承

1、繼承的概念

繼承,是面嚮對象語言的一個重要概念。
一般有這兩種繼承方式:接口繼承實現繼承。接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。面試

《JS高程》裏提到:「因爲函數沒有 簽名,在‘ECMAScript’中沒法實現 接口繼承。」

等等,函數簽名是什麼東西?據MDN文檔定義以下:segmentfault

函數簽名(類型簽名、方法簽名)定義了函數或方法的輸入與輸出。數組

簽名可包含如下內容:函數

  • 參數 及參數的 類型
  • 一個的返回值及其類型
  • 可能會拋出或傳回的異常
  • 該方法在 面向對象程序中的可用性方面的信息(如public、static或prototype)。

看起來好複雜啊,咱們換成強類型的語言的角度會不會更好理解?
譬如,C的函數簽名就是咱們熟悉的函數聲明:this

int func(double d);

此時:參數名爲d,參數類型爲double,返回值爲func(d),返回值類型爲int
再如,Java的函數簽名:prototype

public static void main(String[] args)

此時:參數名爲args,參數類型爲String [],返回值類型爲void因此該方法沒有返回值,訪問修飾符public表示該方法是公有方法,static表示該方法是一個類方法而非實例方法……設計

如今,咱們知道函數簽名是怎麼回事了。那麼,接口繼承又是什麼東西?相信你們會遙想起Java中的interfaceimplements等……在此就不班門弄斧了。code

咱們知道,JavaScript是類型鬆散的語言,不像Java它們有嚴格的變量類型檢查。因此,JS的函數纔沒有簽名,纔沒法實現接口繼承。對象

那麼,JS的實現繼承是怎麼回事呢?繼承

2、JS的繼承

  1. 原型鏈的基本模式
    理解:經過建立SuperType的實例,並將該實例賦給SubType.prototype,來實現SubType繼承SuperTypeinstance的原型指向SubTypeSubType的原型指向SuperTypeSuperType的原型指向Object,如此構成了原型鏈。
    缺點:對象實例共享全部繼承的屬性和方法,不適宜單獨適用

    function SuperType() {
        this.property = true;
    }
    SuperType.prototype.getSuperValue = function() {
        return this.property;
    };
    function SubType() {
      this.subproperty = false;
    }
    //SubType繼承SuperType
    SubType.prototype = new SuperType();
    SubType.prototype.getSubValue = function() {
      return this.subproperty;
    };
    var instance = new SubType();
    alert(instance.getSuperValue());  //true

    因而,有下一個招式,來解決包含引用類型值的原型屬性會被全部實例共享的弱點。

  2. 借用構造函數
    理解:「借用」超類型構造方法,在新的子類型對象上執行超類型函數定義的全部對象初始化代碼
    適用:解決超類型的引用類型值被全部子類型對象實例共享,並且子類型可向超類型傳參
    缺點:不能作到函數複用,從而下降效率,不適宜單獨適用

    function SuperType(name) {
      this.name = name;
      this.colors = ["red", "blue", "green"]; //引用類型值
    }
    function SubType() {
      SuperType.call(this, "A"); //繼承SuperType 「借用」超類型的構造函數 並傳參
      this.age = 20;    //實例屬性
    }
    var instance1 = new SubType();    //instance1.name: "A", instance1.age: 20
    instance1.colors.push("black"); //instance1.colors: "red, blue, green, black"
    var instance2 = new SubType();  //instance2.colors: "red, blue, green"

    接下來,組合技能出大招!

  3. 組合繼承
    理解:將原型鏈和借用構造函數組合到一塊兒,經過原型鏈來繼承共享的原型屬性和方法,經過借用構造函數來繼承實例屬性。
    適用:最經常使用的繼承模式。
    缺點:調用兩次超類型構造函數,在SubType上建立了多餘的屬性,形成超類型對象的實例屬性的重寫

    function SuperType(name) {
      this.name = name;
      this.colors = ["red", "blue", "green"];
    }
    SuperType.prototype.sayName = function() {
      alert(this.name);
    };
    function SubType(name, age) {
      SuperType.call(this, name);   //繼承屬性 第二次調用超類型構造函數 新實例獲得兩個實例屬性name,colors
      this.age = age;
    }
    SubType.prototype = new SuperType();  //繼承方法 第一次調用超類型構造函數 SubType.prototype獲得兩個實例屬性name,colors
    SubType.prototype.sayAge = function() {
      alert(this.age);
    };
    
    var instance1 = new SubType("妹妹", 18);
    instance1.colors.push("black");
    alert(instance1.colors);  //"red, blue, green, black"
    instance1.sayName();  //"妹妹"
    instance1.sayAge();  //18
    
    var instance2 = new SubType("弟弟", 20);
    alert(instance2.colors);  //"red, blue, green"
    instance2.sayName();  //"弟弟"
    instance2.sayAge();  //20

    這是打boss的大招,那我怎麼對付小怪?

  4. 原型式繼承
    理解:本質是對給定對象的淺複製
    適用:沒必要預先定義構造函數來實現繼承,只想讓一個對象與另外一個對象保持相似
    缺點:引用類型值的屬性被共享,如同原型模式同樣

    function object(o) {    //對o進行淺複製
      function F() {}    //建立一個臨時性的構造函數
      F.prototype = o;    //將傳入的對象o做爲F的原型
      return new F();    //返回F的新實例
    }
    
    var person = {
      name: "A",
      friends: ["B", "C", "D"]
    };
    var anotherPerson = object(person);    // Object.create(person)
    anotherPerson.name = "E";
    anotherPerson.friends.push("F");
    var yetAnotherPerson = object(person);    //Object.create(person)
    yetAnotherPerson.name = "G";
    yetAnotherPerson.friends.push("H");
    alert(person.friends);  //"B, C, D, F, H" 引用類型值的屬性被共享啦

    注意:Object.create()方法的第二個參數,新對象的額外屬性的對象,會覆蓋原型對象上的同名屬性。

    var anotherPerson = Object.create(person, {
      name: { value: "E" }
    });
    alert(anotherPerson.name);   // "E"

    打了這麼久,能不能讓小招也升級(封裝)一下啊?

  5. 寄生式繼承

    建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真的是它作了全部工做同樣返回對象。

    理解:繼承的工做是經過調用函數實現的,因此是「寄生」,將繼承工做寄託給別人作,本身只是作加強工做。
    適用:基於某個對象或某些信息來建立對象,而不考慮自定義類型和構造函數。
    缺點:不能作到函數複用,從而下降效率

    function createAnother(original) {
       var clone = object(original); //經過調用函數建立一個新對象
       clone.sayHi = function() {  //以某種方式來加強這個對象
         alert("hi");
       }
       return clone;
     }
     var person = {
       name: "A",
       friends: ["B", "C", "D"]
     };
     var anotherPerson = createAnother(person);  //不只有person全部屬性方法,還有本身的sayHi方法
     anotherPerson.sayHi();  //"hi"

    經驗攢足,我要把以前打boos的組合大招升到滿級!

  6. 寄生組合繼承
    理解:經過借用構造函數來繼承屬性,經過原型鏈的混用形式來繼承方法。用寄生式繼承來繼承超類型的原型,再將加強後的結果指定給子類型的原型。
    適用:引用類型最理想的繼承模式,高效,只調用了一個SuperType構造函數

    function inheritPrototype(subType, superType) {
      var prototype = object(superType.prototype); //建立對象 超類型原型的副本
      prototype.constructor = subType;  //加強對象 彌補因重寫原型而失去默認的constructor屬性
      subType.prototype = prototype;  //指定對象
    }
    
    function SuperType(name) {
      this.name = name;
      this.colors = ["red", "blue", "green"];
    }
    SuperType.prototype.sayName = function() {
      alert(this.name);
    };
    function SubType(name, age) {
      SuperType.call(this, name);   //繼承屬性
      this.age = age;
    }
    inheritPrototype(SubType, SuperType);  //繼承方法
    SubType.prototype.sayAge = function() {
      alert(this.age);
    };

3、ES6的繼承

extends關鍵字用來建立一個普通類或者內建對象的子類。
class A {
    ...
}
class B extends A {
    ...
}

其中,

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

由於extends實現了:

Object.setPrototypeOf(B.prototype, A.prototype);    //B.prototype.__proto__ = A.prototype;

Object.setPrototypeOf(B, A);    //B.__proto__ = A

extends更具體的實現方法參見面試官問:JS的繼承,在此就不班門弄斧了~


完~如有不足,請多指教,不勝感激!

以上代碼借鑑於《JS高級程序設計》
相關文章
相關標籤/搜索