js繼承

原文鏈接

js的繼承有6種方式,大致總結一下它們各自的優缺點,以及它們之間的關係。


1.原型鏈

  js的繼承機制不同於傳統的面嚮對象語言,採用原型鏈實現繼承,基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。理解原型鏈必須先理解原型,以下是對於原型的一些解釋:

無論什麼時候,只要創建了一個新函數,就會根據一組特定規則爲該函數創建一個prototype屬性。這個屬性指向函數的原型對象,所有原型對象都會自動獲得一個constructor屬性,這個屬性是一個指向prototype屬性所在函數的指針。創建自定義的構造函數之後,其原型對象只會取得constructor屬性,其他方法都是從Object繼承來的。當調用構造函數創建一個新實例之後,該實例的內部包含一個指針,指向構造函數的原型對象,即[[Prototype]],在瀏覽器中爲_proto_

  也就是說,構造函數和實例實際上都是存在一個指向原型的指針,構造函數指向原型的指針爲其prototype屬性。實例也包含一個不可訪問的指針[[Prototype]](實際在瀏覽器中可以用_proto_訪問),而原型鏈的形成真正依賴的是_proto_而非[[Prototype]]

舉個例子

下邊是一個最簡單的繼承方式的例子:用父類實例充當子類原型對象。

function SuperType(){                        
    this.property = true;
    this.arr = [1];
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = false;
}
SubType.prototype = new SuperType();              
//在此繼承,SubType的prototype爲SuperType的一個實例
SubType.prototype.getSubValue = function(){
    return this.subproperty;
};
var instance = new SubType();
var instance2 = new SubType();                                   
c(instance.getSuperValue());                      //true
c(instance.getSubValue());                        //false
c(instance.__proto__.prototype);                  //undefined
//SubType繼承了SuperType,SuperType繼承了Object。
//instance的_proto_是SubType的原型對象,即SubType.prototype。
//而SubType.prototype又是SuperType的一個實例。
//則instance._proto_.prototype爲undefined,
//因爲SuperType的實例對象不包含prototype屬性。
instance.arr.push(2);
c(instance.arr);                                  //[1,2]
c(instance2.arr);                                 //[1,2]
//子類們共享引用屬性

需要注意的一點:無論以什麼方式繼承,請謹慎使用將對象字面量賦值給原型的方法,這樣會重寫原型鏈。

優缺點

  原型鏈繼承方式的優點在於簡單,而缺點也十分致命:
1. 子類之間會共享引用類型屬性
2. 創建子類時,無法向父類構造函數傳參


2.借用構造函數

  又叫經典繼承,借用構造函數繼承的主要思想:在子類型構造函數的內部調用超類型構造函數,即用call()apply()方法給子類中的this執行父類的構造函數,使其擁有父類擁有的屬性實現繼承,這種繼承方法完全沒有用到原型。下邊是借用構造函數的實現:

function SuperType(){                                 
    this.colors = ["red","blue","green"];
}
function SubType(){
    SuperType.call(this);     //借用構造函數
}
var instance1 = new SubType();
instance1.colors.push("black");
c(instance1.colors);          //["red","blue","green","black"]
var instance2 = new SubType();
c(instance2.colors);          //["red","blue","green"]

舉個例子

  借用構造函數,相當於將父類擁有的屬性在子類的構造函數也寫了一遍,使子類擁有父類擁有的屬性,這種方法在創建子類實例時,可以向父類構造函數傳遞參數 。

function SuperType(name){           
    this.name = name;
}
function SubType(name){
    SuperType.call(this,name);          //借用構造函數模式傳遞參數
    this.age = 29;
}
var instance = new SubType("something");
c(instance.name);                       //something
c(instance.age);                        //29

優缺點

  借用構造函數模式,不同於原型式繼承和原型模式,它不會共享引用類型屬性,而且也可以向超類型構造函數傳遞參數。但是相對的,由於不會共享屬性,也無法實現代碼複用,相同的函數在每個實例中都有一份。爲了實現代碼複用,提示效率,大神們又想出了下邊的繼承方法。


3.組合繼承

組合繼承有時也叫僞經典繼承,是將原型鏈和借用構造函數的技術組合到一塊,從而發揮二者之長的一種繼承模式。

  即用原型鏈實現對原型屬性和方法的繼承(需要共享的),通過借用構造函數實現對實例屬性的繼承(不共享的)。這樣的方法實現了函數複用,而且每個實例擁有自己的屬性。

舉個例子

function SuperType(name) {                       //父類的實例屬性
    this.name = name;
    this.colors = ["red", "blue", "green"];      
}
SuperType.prototype.sayName = function() {       //父類的原型屬性
    c(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() {
    c(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
c(instance1.colors);        //"red,blue,green,black"
delete instance1.colors;    
//刪除從實例屬性繼承來的colors,讀取colors會成爲從原型繼承來的實例屬性
c(instance1.colors);        //"red,blue,green"
instance1.sayName();        //Nicholas
instance1.sayAge();         //29
var instance2 = new SubType("Greg", 27);
c(instance2.colors);        //"red,blue,green"
instance2.sayName();        //Greg
instance2.sayAge();         //27

優缺點

這是所有繼承方式中最常用的,它的優點也十分明顯:
1. 可以在創建子類實例時向父類構造函數傳參。
2. 引用類型屬性的值可以不共享。
3. 可以實現代碼複用,即可以共享相同的方法。

但是這種方法依然有一點不足,調用了兩次父類的構造函數,最後會講到一種理論上接近完美的繼承方式,即寄生組合式繼承。


4.原型式繼承

  原型式繼承藉助原型基於已有對象創建新對象,需要一個對象作爲另一個對象的基礎:

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

  上邊段代碼就是原型式繼承的核心代碼,先創建一個臨時性的構造函數,然後將傳入的對象作爲這個構造函數的原型,最後返回這個臨時類型的一個新實例。

舉個例子

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);      //在此繼承
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
c(person.friends);              //["Shelby","Court","Van","Rob","Barbie"]
c(person.name);                 //Nicholas
c(anotherPerson.name);          //Greg
c(yetAnotherPerson.name);       //Linda
delete yetAnotherPerson.name;   
//刪除子類的屬性,就會解除對父類屬性的屏蔽,暴露出父類的name屬性
c(yetAnotherPerson.name);       //Nicholas

  從上邊的代碼顯示,由object(注意首字母小寫,不是對象的構造函數)產生的兩個子類會共享父類的引用屬性,其中friends數組是共享的,anotherPerson和yetAnotherPerson都是繼承自person。實際上相當於創建了兩個person對象的副本,但可以在產生之後擁有各自的實例屬性。

ECMAScript5新增了Object.create()方法規範化了原型式繼承,這個方法接受兩個參數:
1. 一個作爲新對象原型的對象(可以是對象或者null)
2. 另一個爲新對象定義額外屬性的對象(可選,這個參數的格式和Object.defineProperties()方法的第二個參數格式相同,每個屬性都是通過自己的描述符定義的)

  下邊是一個例子:

var person = {                      //原型式繼承規範化爲create()函數
    name: "Nicholas",
    friends: ["Shelby","Court","Van"]
};
var anotherPerson = Object.create(person, {
    name: {
        value: "Greg"
    }
});
c(anotherPerson.name);             //"Greg"

優缺點

  如果想讓一個對象與另一個對象保持類似,原型式繼承是很貼切的,但是與原型模式一樣,包含引用類型的值得屬性會共享相應的值。


5.寄生式繼承

寄生式繼承與原型式繼承緊密相關的一種思路,與寄生構造函數和工廠模式類似,即創建一個僅用於封裝繼承過程的函數,函數內部以某種方式來增強對象,最後再像真的做了所有工作一樣返回對象。

舉個例子

function createAnother(original) { 
    var clone = object(original);          //此處用到了原型式繼承
    clone.sayHi = function() {
        c("Hi");
    };
    return clone;
}
var person = {                             //父類實例
    name: "Nicholas",
    friends: ["Shelby","Court","Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi();

  上邊的寄生式繼承用到了原型式繼承,向實現繼承的函數傳入一個父類對象實例,再用原型式繼承得到一個父類對象實例的副本,再給這個副本添加屬性,即增強這個對象,最後返回這個副本對象。由於用到了原型式繼承,這個對象的原型指向傳入的父類對象實例。上邊例子用到的object()函數(原型式繼承)並不是必須的,任何能夠返回新對象的函數都適用於寄生式繼承模式

優缺點

  寄生式繼承在主要考慮對象而不是創建自定義類型和構造函數時,是十分有用的。但是如果考慮到用寄生式繼承爲對象添加函數等,由於沒有用到原型,做不到函數複用,會導致效率降低。


6.寄生組合式繼承

  這個名字並不是很貼切,雖然叫寄生組合式繼承,但是和寄生式繼承關係不是很大,主要是用原型式繼承來實現原型屬性的繼承,用借用構造函數模式繼承實例屬性。寄生組合式繼承和組合繼承的區別在於:
1. 在繼承原型屬性時,組合繼承用原型鏈繼承了整個父類(通過將父類實例賦值給子類構造函數的原型對象來實現),這使子類中多了一份父類的實例屬性。而寄生組合式繼承用原型式繼承只繼承了父類的原型屬性(把父類構造函數的原型對象用原型式繼承複製給子類的構造函數的原型對象)。
2. 組合繼承調用了兩次超類型構造函數,寄生組合式繼承調用了一次。

舉個例子

function inheritPrototype(subType, superType) {             //寄生式繼承
    var prototype = Object.create(superType.prototype);     //創建對象
    prototype.constructor = subType;                        //增強對象
    subType.prototype = prototype;                          //指定對象
}
function SuperType(name) {
    this.name = name;
    this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
    c(this.name);
};
function SubType(name, age) {
    SuperType.call(this, name);                 //借用構造函數
    this.age = age;                             //添加子類獨有的屬性
}
inheritPrototype(SubType, SuperType);           //此處調用實現寄生組合繼承的函數
SubType.prototype.sayAge = function() {         //添加子類獨有原型屬性
    c(this.age);
};
var son = new SubType("erzi",16);
var father = new SuperType("baba");
c(typeof father.sayName);                       //function
c(typeof father.sayAge);                        //SubType獨有的方法,返回undefined
SubType.prototype.sayName = function() {        
    c("This function has be changed");          
}   
//更改子類的方法只會影響子類,prototype是對象,添加新屬性和更改屬性不會影響父類的prototype
father.sayName();                               //baba
son.sayName();                                  //This function has be changed
SuperType.prototype.sayName = function() {      //更改父類的原型屬性
    c("This function has be changed");
}
father.sayName();                               //This function has be changed
son.sayName();                                  //This function has be changed

優缺點

  這種繼承方式理論上是完美的,但是由於出現的較晚,人們大多數使用的是組合繼承模式。


  以上就是我對於js繼承的一些理解,如果你有不一樣的想法歡迎討論。

參考資料:《JavaScript高級程序設計》

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