讀《javaScript高級程序設計-第6章》之繼承

讀這篇以前,最好是已讀過我前面的關於對象的理解和封裝類的筆記。
第6章我一共寫了3篇總結,下面是相關連接:
讀《javaScript高級程序設計-第6章》之理解對象
讀《javaScript高級程序設計-第6章》之封裝類java

1、原型鏈

原型鏈最簡單的理解就是:原型對象指向另外一個構造函數的實例。此時的原型對象包括一個指向另外一個原型的指針,相應的,另外一個原型中的constructor指向另外一個構造函數。這種關係層層遞進,就經過一個原型對象連接另外一個構造函數的原型對象的方式實現了繼承。
下面用代碼和圖來詳細分析一下原型鏈中的各類關係:segmentfault

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

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

function SubType(){
    this.subproperty = false;
}

//inherit from SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function (){
    return this.subproperty;
};

var instance = new SubType();
alert(instance.getSuperValue());   //true
alert(instance.getSubValue());   //false
alert(instance instanceof Object);      //true
alert(instance instanceof SuperType);   //true
alert(instance instanceof SubType);     //true

alert(Object.prototype.isPrototypeOf(instance));    //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance));   //true
console.log(new SuperType());
console.log(instance);

下圖是上面代碼中打印出來的new SuperType()和instance的分析:

從上面的分析咱們看到的原型鏈:
SubType的原型裏有指向SuperType的原型的指針,SuperType的原型裏有指向Object的原型的指針。
也能夠看紅皮書裏的圖:
ide

  • 訪問屬性的搜索過程:

當以讀取模式訪問一個構造函數(SubType)的實例的屬性時,首先會在實例中搜索實例屬性。若是沒找到該屬性,則會繼續搜索實例的原型;SubType繼承了SuperType,那麼實例的原型是另外一個構造函數(SuperType)的實例,搜索實例的原型也就是在SuperType的實例中搜索該屬性,沒找到繼續搜索SuperType的原型;SuperType繼承了Object,以此遞進,一層層搜索,直到找到或者搜到了原型鏈的末端停下來。函數

  • 判斷原型和實例的關係

(1)instanceof
實例的原型鏈中出現過待檢測的構造函數,就會返回truethis

alert(instance instanceof Object);      //true
alert(instance instanceof SuperType);   //true
alert(instance instanceof SubType);     //true

(2)isPrototypeOf()方法
待檢測對象出如今instance的原型鏈中,就會返回truespa

alert(Object.prototype.isPrototypeOf(instance));    //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance));   //true
  • 注意事項

(1)給原型添加方法的代碼必定要放在替換原型的語句以後。也就是prototype

SubType.prototype = new SuperType();這句代碼必定要先寫,在寫下面的代碼
//new method
SubType.prototype.getSubValue = function (){
    return this.subproperty;
};

//override existing method
SubType.prototype.getSuperValue = function (){
    return false;
};

(2)在經過原型鏈實現繼承時,不能使用對象字面量爲原型添加屬性,由於這會重寫原型鏈(具體請看理解對象篇裏的1、建立對象)。
以下:設計

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

    someOtherMethod : function (){
        return false;
    }
};

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

其實這兩個注意事項,只要你明白了(理解對象篇裏的1、建立對象)後,根本不須要解釋。指針

  • 原型鏈的問題

(1)沒有辦法在不影響全部對象實例的狀況下,給超類型的構造函數傳遞參數。
(2)在另外一篇筆記封裝類原型模式中提到過,原型中的屬性是被共享的,但若是屬性的值時引用類型,會有問題的。而在繼承時,原型實際上會是另外一個類型的實例(這個實例包含引用類型值的實例屬性),那麼原先的這個實例的實例屬性就會成爲如今的原型屬性了,就會出現一樣的問題了。共享了引用類型值的屬性。code

2、借用構造函數

直接上代碼吧:

function SuperType(name){
    this.name = name;
}

function SubType(){ 
    //繼承了 SuperType ,同時還傳遞了參數
    SuperType.call(this, "Nicholas");
   
    //實例屬性
    this.age = 29;
}

var instance = new SubType();
alert(instance.name);    //"Nicholas";
alert(instance.age);     //29

如上寫法就解決了原型鏈裏的兩個問題了,爲何呢?請看下面的講解:
SuperType,若是你用new調用它是構造函數,但你不用new,它就是個普通函數。SuperType.call(this, "Nicholas");不但傳遞了參數,還綁定了子類的做用域,就至關於SuperType方法在幫助定義子類的實例屬性。也就是說,即便SuperType的中定義的屬性裏有引用類型值,也不會成爲子類SubType的原型屬性,仍然時實例屬性。咱們要時刻記住實例屬性是每一個實例所私有的,而原型屬性是會被全部實例所共享的。

固然這也寫也不完美,問題顯而易見,和構造函數模式一樣的問題。

3、組合繼承

組合繼承,就像是封裝類裏的把構造函數模式和原型模式組合使用是同樣的。這裏是把原型鏈和借用構造函數相組合。
簡單來講就是:使用原型鏈實現對原型屬性和方法的繼承,經過借用構造函數實現對實例屬性的繼承(父類的實例屬性變成子類的實例屬性)。
仍是上代碼吧:

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.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

解釋:
下圖是instance1的打印

咱們能夠看到instance1具備了父類SuperType的實例屬性name 、colors,可是子類的原型是父類的實例,因此原型中仍存在父類的實例屬性,可是子類已經有了一樣的實例屬性name和colors,因此子類原型中的這兩個屬性就被屏蔽了。從子類訪問它的name和colors屬性只會訪問到它的實例屬性。

組合繼承是javaScript中最經常使用的繼承模式。並且instance和isPrototypeOf()也可以用於識別給予組合繼承建立的對象類型。

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");
console.log(person.friends);   //"Shelby,Court,Van,Rob,Barbie」

你們還記得原型模式嗎。個人理解:這就是一個原型模式,區別是object這個方法就至關於一個工廠,你傳給它一個對象,它就給你一個原型是這個對象的實例。這個實例就會相應的繼承到了你傳給它的那個對象的屬性。
固然你也能夠不用本身寫上面的object這個方法,由於ES5提供了,並且更規範。ES5中新增了Object.create()方法規範化了原型式繼承。這個方法接受兩個參數:一個是用作新對象原型的對象和(可選)一個爲新對象定義額外屬性的對象(或者說是定義新對象的實例屬性的對象,這個參數和defineProperties()方法的第二個參數格式相同:每一個屬性都是經過本身的描述符定義的)
上代碼:

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
                  
var anotherPerson = Object.create(person, {
    name: {
        value: "Greg"
    }
});
console.log(anotherPerson);

打印結果圖:

從上圖能夠看到第二個參數定義的name屬性是新對象的實例屬性,它會屏蔽掉它的原型屬性裏的同名屬性name。簡單來講,Object.create就是用原型模式建立新對象的一個工廠,第一個參數定義了原型屬性,第二個參數定義了實例屬性。

5、寄生式繼承

這一小節,感興趣瞭解一下。

6、寄生組合式繼承

前面說過,組合繼承是js裏最經常使用的繼承模式,可是它並不完美。問題是:調用了兩次超類SuperType的構造函數,子類建立了一部分多餘的屬性(這部分屬性是超類的實例屬性,在子類的實例屬性裏存在並有用,但在子類的原型中也存在且沒用)。寄生組合式繼承就是解決這個問題的。
上代碼:

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

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype);   //create object            
    prototype.constructor = subType;         //augment object    
    subType.prototype = prototype;         //assign object
}
                       
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);
};

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

console.log(instance1);
console.log(SuperType.prototype)

代碼運行打印結果圖:

從圖中能夠看到instance1(子類實例)的原型裏已經沒有了超類的實例屬性name、colors。並且代碼中只運行了一次超類構造函數。怎麼作到的呢?請看下面的解釋:
咱們先看這段代碼:

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype);   //create object
    prototype.constructor = subType;               //augment object
    subType.prototype = prototype;                 //assign object
}

subType的原型仍是指向了一個對象,這個對象是什麼呢?object這個方法返回的對象,這個對象是一個構造函數是空的,原型指向超類原型的實例。什麼意思呢?就是說subType的原型仍是一個構造函數的實例,但不是超類SuperType的實例,而是一個新建的臨時的空的構造函數F的實例。看代碼:

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

這個臨時的構造函數F具備和超類SuperType同樣的原型。那麼這個時候的子類的原型中就只有F的實例屬性和原型,而F的實例屬性是空的,就只有F的原型,F的原型就是超類SuperType的原型。這樣子類的實例屬性仍是繼承了超類的實例屬性,而子類的原型屬性只繼承了超類的原型。完美,就這樣。

囉嗦一句我對面向對象程序設計的理解,面向對象程序設計就是一直在說如何使用對象。其實,只要結果符合你的預期,對象真的是想怎麼使用就怎麼使用,不必定非得像書中說的什麼各類模式的。固然書中的這麼多種模式方法的介紹能夠了解一下(可是構造函數模式、原型模式。以及繼承裏的原型鏈、借用構造函數。還包括它們的組合使用仍是須要認真研讀,深入理解的。再順便說一句,繼承裏的原型鏈、借用構造函數能夠看做是原型模式和構造函數模式的進化),能夠加深本身對對象的理解,有助於你花式使用對象的方法。

相關文章
相關標籤/搜索