JS學習筆記(第6章)(面向對象之繼承——JS繼承的六大方式)

一、原型鏈

原型鏈的基本思想是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。
構造函數、原型和實例的關係:每一個構造函數都有一個原型對象;原型對象都包含着一個指向構造函數的指針;實例都包含一個指向原型對象的內部指針。若是咱們讓原型對象等於另外一個類型的實例,此時的原型對象將包含一個指向另外一個原型的指針,相應地另外一個原型中也包含着指向另外一個構造函數的指針……層層遞進,就構成了實例與原型的鏈條,這就是所謂原型鏈的基本概念。app

clipboard.png

實現原型鏈有一種基本模式,其代碼大體以下:函數

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

(1)不要忘記默認的原型

clipboard.png

(2)肯定原型和實例的關係

能夠經過兩種方式來肯定原型和實例之間的關係。
1)使用instanceof操做符,只要用這個操做符來測試實例與原型鏈中出現過的構造函數,結果就會返回true。測試

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

2)使用isPrototypeOf()方法。只要是原型鏈中出現過的原型,均可以說是該原型鏈所派生的實例的原型,所以isPrototypeOf()方法也會返回true。this

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

(3)謹慎地定義方法

子類型有時候須要覆蓋超類型中的某個方法或者須要添加超類型中不存在的某個方法。但無論這樣,給原型添加方法的代碼必定要放在替換原型的語句以後spa

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;
};
//重寫超類型中的方法
SubType.prototype.getSuperValue = function() {
    return false;
};
var instance = new SubType();
alert(instance.getSuperValue());  //false

在經過原型鏈實現繼承時,不能使用對象字面量建立原型方法。由於這樣作就會重寫原型鏈。會致使實例與原型鏈之間的聯繫被切斷。prototype

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

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

//使用字面量添加新方法,會致使上一行代碼無效
SubType.prototype = {
    getSubValue : function() {
        return this.subproperty;
    },
    sonOtherMethod : function() {
        return false;
    }
};
var instance = new SubType();
alert(instance.getSuperValue());  //error!

(4)原型鏈的問題

1)最主要的問題來自包含引用類型值的原型,包含引用類型值的原型屬性會被全部實例共享;
2)第二個問題是:在建立子類型的實例時,不能向超類型的構造函數中傳遞參數;3d

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

function SubType(){

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

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

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

如上所示,咱們對instance1.colors的修改可以經過instance2.colors反映出來。指針

二、借用構造函數

思想:在子類型構造函數的內部調用超類型構造函數。
函數只不過是在特定環境中執行代碼的對象,所以經過使用apply()和call()方法也能夠在新建立的對象上執行構造函數。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 SuperType();
alert(instance2.colors);   //"red,blue,green"

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

(1)傳遞參數

相對於原型鏈而言,借用構造函數的一個很大優點在於:能夠在子類型構造函數中向超類型構造函數傳遞參數。

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(intance.age);  //29

爲了確保SuperType構造函數不會重寫子類型的屬性,能夠在調用超類型構造函數後,再添加應該在子類型中定義的屬性。

(2)借用構造函數的問題

若是僅僅是借用構造函數,那麼也就沒法避免構造函數模式存在的問題——方法都在構造函數中定義,所以函數複用就無從談起。除此以外,在超類型的原型中定義的方法,對子類型而言也是不可兼得,結果全部類型都只能用構造函數模式。

三、組合繼承

將原型鏈和借用構造函數的技術組合到一塊兒。
思想:使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。
這樣,既經過在原型上定義方法實現了函數複用,又能保證每一個實例都有它本身的屬性。

function SuperType(name) {
    this.name = name;
    this.color = ["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

instanceof()和isPrototypeOf()也可以用於識別基於組合繼承建立的對象、

四、原型式繼承

藉助原型能夠基於已有的對象建立新對象,同時還沒必要所以建立自定義類型

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

在object()函數內部,先建立了一個臨時性的構造函數,而後將傳入的對象做爲這個構造函數的原型,最後返回這個臨時類型的一個新實例。

var person = {
    name : "Nicholas",
    friends : ["Shelby", "Court", "Van"]
};
var anotherPerson = object(perosn);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAbotherPerson = object(perosn);
yetAbotherPerson.name = "Linda";
yetAbotherPerson.firends.push("Barbie");

alert(person.friends);  //"Shelby, Court, Van, Greg, Linda"

clipboard.png

ECMAScript5經過新增Object.creat()方法規範了原型式繼承。這個方法接收兩個參數:用做新對象原型的對象和爲新對象定義額外屬性的對象(可選)。在傳入一個參數的狀況下,Object.create()與object()方法的行爲相同。

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

var yetAbotherPerson = Object.create(perosn);
yetAbotherPerson.name = "Linda";
yetAbotherPerson.firends.push("Barbie");

alert(person.friends);  //"Shelby, Court, Van, Greg, Linda"

clipboard.png
Object.create()方法的第二個參數與Object.defineProperties()方法的第二個參數格式相同:每一個屬性都經過本身的描述定義的。用這種方式指定的任何屬性都會覆蓋原型對象上的同名屬性。

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

五、寄生式繼承

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

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

基於person返回一個新對象——anotherPerson。新對象不只具備Person的全部屬性和方法,還有本身的sayHi方法。

function creatAnother(original) {
     var clone = object(original);   //經過調用函數建立一個新對象
     clone.sayHi = function() {      //以某種方式來加強這個對象
         alert("hi");
     };
     return clone;  //返回這個對象
 }
 var person = {
    name : "Nicholas",
    friends : ["Shelby", "Court", "Van"]
};

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

六、寄生組合式繼承

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

function SuperType(name) {
    this.name = name;
    this.color = ["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);
};

clipboard.png
寄生式組合繼承,就是經過借用構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法。其思想是:沒必要爲了指定子類型的原型而調用超類型的構造函數,咱們所需的無非就是超類型原型的一個副本而已。本質上就是,使用寄生式繼承來繼承超類型的原型,然再將結果指定給子類型的原型。其基本模式以下:

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

這個函數接收兩個參數:子類型構造函數和超類型構造函數。
第一步是建立超類型原型的一個副本;
第二步是爲建立的副本添加constructor屬性,從而彌補因重寫原型而失去的默認的constructor屬性;
第三步將新建立的對象(即副本)賦值給子類型的原型。

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

function SuperType(name) {
    this.name = name;
    this.color = ["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);
};

clipboard.png
這個例子的高效率體如今它只調用了一次SuperType構造函數。

七、總結

JavaScript主要經過原型鏈實現繼承。原型鏈的構建是經過將一個類型的實例賦值給另外一個構造函數的原型實現的。SubType.prototype=new SuperType();這樣,子類型就可以訪問超類型的全部屬性和方法,這一點與基於類的繼承很類似。原型鏈的問題是對象實例共享全部繼承的屬性和方法,所以不適宜單獨使用。解決這個問題的技術是借用構造函數,即在子類型構造函數的內部調用超類型構造函數SuperType.call(this,name); 這樣就能夠作到每一個實例都具備本身的屬性,同時還能保證只使用構造函數模式來定義類型。使用最多的繼承模式是組合繼承,這種模式使用原型鏈繼承共享的屬性和方法,而借用構造函數繼承實例屬性。

此外,還存在下列可供選擇的繼承模式:

  • 原型式繼承,能夠在沒必要預先定義構造函數的狀況下實現繼承,其本質是執行給定對象的淺複製。而複製的副本還能夠獲得進一步的改造。
  • 寄生式繼承,與原型式繼承很是類似,也是基於某個對象或某些信息建立一個對象,而後加強對象,最後返回對象。爲了解決組合繼承模式因爲屢次調用超類型函數而致使的低效率問題,能夠將這個模式與組合繼承一塊兒使用。
  • 寄生組合式繼承,集寄生式繼承與組合繼承的優勢於一身,是實現基於類型繼承的最有效的方式
相關文章
相關標籤/搜索