轉載自:原型prototype[2]
前面說了介紹了原型,這一部分說說原型的一大作用:繼承。在面向對象中,繼承有兩種方法,分別是接口繼承和實現繼承。但是在JavaScript中,函數不能只簽名,所以JavaScript中只有實現繼承。
JavaScript繼承的基本思路就是讓一個引用類型繼承另一個引用類型的屬性和方法。
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();
console.log(instance.getSupterValue()); //print: true
首先我們先來看下上面函數之間的關係,如下圖:
1 由上面的圖我們可以看到,繼承其實就是通過改寫原型實現的。我們將SuperType的一個實例賦值給SubType.prototype,使得SubType.prototoype含有指向SuperType.prototype的指針,這樣就將原型之間的關係聯繫起來,構成了原型鏈(prootype chain)。
2 注意觀察,可以發現在SubType.prototype原型中含有SuperType構造函數中的屬性property。這是因爲SubType.prototype是作爲SuperType的一個實例,所以自然保存有SuperType實例中的屬性,但是對於getSuperValue方法則還是保存在SuperType.prototype中。
3 SubType.prototype中含有的屬性都是共享的,所以對於property屬性,如果SubType有多個實例則是共享該屬性值。
4 原型繼承之後的原型搜索,還是和之前一樣,先搜索實例對象的屬性,然後在沿着原型鏈逐步向上搜索,如果最後還是沒有找到則直接返回undefined。
5 在JavaScript中,所有的對象都是繼承自Object,所以所有函數的默認原型都是Object的實例,因此默認原型都會有一個內部指針,指向Object.prototype。這也是爲什麼每一個函數都會有toString(), valueOf()等默認方法的基本原因。 6 對於使用原型繼承的時候,不能使用字面量的形式來重寫原型,如:
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();
console.log(instance.getSuperValue()); //print: error!找不到該方法
1 上面的代碼首先將SubType.prototype賦值爲SuperType的實例,這個時候是建立了繼承關係;但是在後面又重寫了SubType.prototype,這時候SubType.prototype變成了Object對象的一個實例,因此SubType和SuperType之間的關係已經被切斷,兩者之間沒有了關係。
原型鏈存在的問題
function SuperType(){
this.color = ['red', 'blue', 'green'];
};
function SubType(){}
//繼承了SuperType
SubType.prototype = new SuperType();
var instanceFirst = new SubType();
instanceFirst.color.push('black');
console.log(instanceFirst.color); //print: ['red', 'blue', 'green', 'black']
var instanceSecond = new SubType();
console.log(instanceSecond.color); //print: ['red', 'blue', 'green', 'black']
首先還是來看下函數原型之間的關係,如下圖:
1 由上面的圖可以看到,由於SubType.prototype是作爲SuperType的實例,所以在SubType.prototype中保存有一份SuperType實例對象都具有的屬性,即color屬性。這就會導致SubType的所有實例都共享一份color;
2 又因爲在原型中所有屬性都是共享的,所以在SubType的實例中都能引用到color的屬性值。因此我們可以看到在instanceFirst和instanceSecond都是共享同一個color數組,這也就是爲什麼instanceFirst改變了color的值之後,instanceSecond的color數組也跟着改變的原因。
3 原型鏈繼承存在的問題:通過原型繼承,原型實際上是變成另一個類型的實例,於是原先的實例屬性也就順理成章的變成了現在原型的屬性。
原型鏈問題的解決方法
1 對於從SuperType構造函數繼承下來的屬性到原型中的問題,可以通過在SubType的構造函數中調用SuperType構造函數。可以通過call和apply來實現。
function SuperType(){
this.colors = ['red', 'blue', 'green'];
};
function SubType(){
//繼承SuperType
//在this的作用域上調用SuperType構造函數
SuperType.call(this);
};
var instanceFirst = new SubType();
instanceFirst.colors.push('black');
console.log(instanceFirst.colors); //print: ['red', 'blue', 'green', 'black']
var instanceSecond = new SubType();
console.log(instanceSecond.colors); //print: ['red', 'blue', 'green']
上面的這種方法通常稱之爲經典繼承方法,但是該方法還是存在構造函數模式的缺點,即所有的方法都在構造函數中定義,因此函數的複用就無從談起。因此很多情況下都是使用下面的一種方法:組合繼承。
2 組合繼承即在上面的方法中添加如原型鏈的形式。
function SuperType(name){
this.name = name;
this.colors = ['red', 'blue', 'green'];
};
SuperType.prototype.sayName = function(){
console.log(this.name);
};
function SubType(name, age){
//繼承SuperType屬性
SuperType.call(this, name);
this.age = age;
};
SubType.prototype = new SuperType();
SubType.prototype.sayAge(){
console.log(this.age);
};
var instanceFirst = new SubType("Louis", 23);
instanceFirst.colors.push('black');
console.log(instanceFirst.colors); //print: ['red', 'blue', 'green', 'black']
console.log(instanceFirst.sayName()); //print: Louis
console.log(instanceFirst.sayAge()); //print: 23
var instanceSecond = new SubType("June", 21);
console.log(instanceSecond.colors); //print: ['red', 'blue', 'green']
console.log(instanceSecond.sayName()); //print: June
console.log(instanceSecond.sayAge()); //print:21
這種方法避免了原型鏈和經典繼承的弊端,因此較爲常用。