混子前端所知道關於JS的六種繼承方式

繼承是OO語言中的一個最爲人津津樂道的概念。許多OO語言都支持兩種繼承方式:接口繼承和實現繼承。接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。前端

---本文摘選《JavaScript高級程序設計(第3版)》bash


因爲函數沒有簽名,在ECMAScript中沒法實現接口繼承。ECMAScirpt只支持實現繼承,並且其實現繼承主要依靠原型鏈來實現的。app

原型鏈

ECMAScript中描述了原型鏈做爲實現繼承的主要方法。函數

基本思想:利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。
ui

PS:每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。
this

若是讓原型對象等於另外一個類型的實例,此時原型對象將包含一個指向另外一個原型的指針,另外一個原型中也包含着一個指向另外一個構造函數的指針。另外一個原型又是另外一個類型的實例,如此層層遞進,就構成了實例與原型的鏈條。
spa

JS code:

function SuperType(){
    this.property = true;
};
SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}
// 繼承了SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
    return this.subproperty;
};

var instance = new SubType();
alert(instance.getSuperValue());    // true

複製代碼

以上代碼定義了兩個類型:SuperType 和 SubType。每一個類型分別有一個屬性和一個方法。它們 的主要區別是 SubType 繼承了 SuperType,而繼承是經過建立 SuperType 的實例,並將該實例賦給 SubType.prototype 實現的。實現的本質是重寫原型對象,代之以一個新類型的實例。 prototype

注意:instance.constructor 如今指向的 是 SuperType,這是由於原來 SubType.prototype 中的 constructor 被重寫了的緣故。設計

不足經過原型來實現繼承時,原型實際上會變成另外一個類型的實例。因而,原先的實例屬性也就瓜熟蒂落地變成了如今的原型屬性了。 下面代碼能夠說明這個問題:指針

JS code:

function SuperType(){
    this.colors = ["red", "blue", "green"];
}

function SubType(){
}

SubType.prototype = new SuperType();    //繼承了 SuperType

var instance1 = new SubType(); 
instance1.colors.push("black"); 
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType(); 
alert(instance2.colors); //"red,blue,green,black"

複製代碼

PS:在建立子類型的實例時,不能向超類型的構造函數中傳遞參數;這也是原型鏈繼承的不足。 


借用構造函數 [僞造對象或經典繼承]

基本思想:在子類型構造函數的內部調用超類型構造函數。函數只不過是在特定環境中執行代碼的對象,所以經過使用 apply()和 call()方法也能夠在(未來)新建立的對象上執行構造函數。

JS code:

function SuperType(){
    this.colors = ["red", "blue", "green"];    
}

function SubType(){    
    //繼承了 SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);    //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors);    //"red,blue,green"

複製代碼

以上代碼經過使用 call()方法[ 或 apply()方法 ],咱們其實是在新建立的 SubType 實例的環境下調用了 SuperType 構造函數。 這樣就會在新 SubType 對象上執行 SuperType()函數中定義的全部對象初始化代碼。結果, SubType 的每一個實例就都會具備本身的 colors 屬性的副本了。 

PS:相對於原型鏈而言,借用構造函數有一個很大的優點,便可以在子類型構造函數中向超類型構造函數傳遞參數,這裏就不舉例說明,只是call()方法[ 或 apply()方法 ]的特性。

不足:方法都在構造函數中定義,所以函數複用就無從談起了。並且,在超類型的原型中定義的方法,對子類型而言也是不可見的,結果全部類型都只能使用構造函數模式。 


組合繼承 [僞經典繼承]

基本思想:將原型鏈和借用構造函數的 技術組合到一塊,從而發揮兩者之長的一種繼承模式。背後思路是使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。 

JS code: 

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

//繼承方法
SubType.prototype = new SuperType(); 
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){
    alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors);    //"red,blue,green,black"
instance1.sayName();    //"Nicholas";
instance1.sayAge();    //29

var instance2 = new SubType("Greg", 27); 
alert(instance2.colors);    //"red,blue,green"
instance2.sayName();    //"Greg";
instance2.sayAge();    //27

複製代碼

SuperType 構造函數定義了兩個屬性:name 和 colors。SuperType 的原型定義了一個方法 sayName()。SubType 構造函數在調用 SuperType 構造函數時傳入了 name 參數,緊接着又定義了它本身的屬性 age。而後將 SuperType 的實例賦值給 SubType 的原型,而後又在該新原型 上定義了方法 sayAge()。這樣一來,就可讓兩個不一樣的 SubType 實例既分別擁有本身屬性——包括 colors 屬性,又可使用相同的方法了。 

組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優勢,成爲 JavaScript 中最經常使用的繼承模式。並且,instanceof 和 isPrototypeOf()也可以用於識別基於組合繼承建立的對象。 


原型式繼承 

基本思想:藉助原型能夠基於已有的對象建立新對象,同時還沒必要所以建立自定義類型。參考以下函數:

JS code:

function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}

複製代碼

在 object()函數內部,先建立了一個臨時性的構造函數,而後將傳入的對象做爲這個構造函數的原型,最後返回了這個臨時類型的一個新實例。從本質上講,object()對傳入其中的對象執行了一次淺複製。 

PS:這種原型式繼承要求必須有一個對象能夠做爲另外一個對象的基礎。若是有這麼 一個對象的話,能夠把它傳遞給 object()函數,而後再根據具體需求對獲得的對象加以修改便可。 

而ECMAScript 5 經過新增 Object.create()方法規範化了原型式繼承。

這個方法接收兩個參數

  1. 用做新對象原型的對象和(可選的)
  2. 一個爲新對象定義額外屬性的對象。
在傳入一個參數的狀況下, Object.create()與 object()方法的行爲相同。 

JS code:

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends);    //"Shelby,Court,Van,Rob,Barbie"

複製代碼

PS:Object.create()方法的第二個參數與 Object.defineProperties()方法的第二個參數格式相

同,這裏不作解釋。

適用場景:只想讓一個對象與另外一個對象保持相似的狀況下,原型式繼承是徹底能夠勝任的,不過包含引用類型值的屬性始終都會共享相應的值,就像使用原型模式同樣。 


寄生式繼承 

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

JS code:

function createAnother(original){ 
    var clone = object(original);    //經過調用函數建立一個新對象
    clone.sayHi = function(){    //以某種方式來加強這個對象
    alert("hi");
};
    return clone;    //返回這個對象
}

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi();    //"hi"

複製代碼

基於 person 返回了一個新對象 [ anotherPerson ];新對象不只具備 person 的全部屬性和方法,並且還有本身的 sayHi()方法。 

適用場景:在主要考慮對象而不是自定義類型和構造函數的狀況下,寄生式繼承也是一種有用的模式。 


寄生組合式繼承 

這裏拿出組合式繼承說明問題:不管什麼狀況下,都會調用兩次超類型構造函數:一次是在建立子類型原型的時候,另外一次是在子類型構造函數內部。 子類型最終會包含超類型對象的所有實例屬性,但咱們不得不在調用子類型構造函數時重寫這些屬性。 

如下是組合式繼承:

JS code:

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);    // 第二次調用SuperType()
    this.age = age;
}
SubType.prototype = new SuperType();    // 第一次調用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
};

複製代碼

第一次調用 SuperType 構造函數時,SubType.prototype 會獲得兩個屬性:name 和 colors;它們都是 SuperType 的實例屬性,只不過如今位於 SubType 的原型中;

當調用 SubType 構造函數時,會第二次調用SuperType 構造函數,這一次又在新對象上建立了實例屬性 name 和 colors

因而,這兩個屬性就屏蔽了原型中的兩個同名屬性。 

[寄生組合式繼承]基本思想:經過借用構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法。 

JS code:function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype);    // 建立對象
    prototype.constructor = subType;    // 加強對象
    subType.prototype = prototype;  // 指定對象
}

複製代碼

這個函數接收兩個參數:子類型構造函數 和 超類型構造函數。

在函數內部,第一步是建立超類型原型的一個副本。

第二步是爲建立的副本添加 constructor 屬性,從而彌補因重寫原型而失去的默認的 constructor 屬性。 

最後一步,將新建立的對象(即副本)賦值給子類型的原型。

這樣,咱們就能夠用調用 inheritPrototype()函數的語句,去替換前面例子中爲子類型原型賦值的語句了。

JS code:

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

複製代碼

PS:寄生組合式繼承高效率體如今它只調用了一次 SuperType 構造函數,而且所以避免了在 SubType. prototype 上面建立沒必要要 多餘的屬性,同時,原型鏈還能保持不變;

因此開發人員廣泛認爲寄生組合式繼承是引用類型最理想的繼承範式。


ESMA5 的繼承能夠用下圖來歸納:


好了,以上就是混子前端所知道 ECMA5 中的六種繼承方式,這裏省略 ECMA6 中class繼承,歡迎大神留言填充。


最後祝你們週末愉快 !

相關文章
相關標籤/搜索