ECMAScript只支持實現繼承,主要依靠原型鏈來實現。與實現繼承對應的是接口繼承,由於script中函數沒有簽名,所以無法實現接口繼承。
一、原型鏈
基本思想:利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。
構造函數、原型和實例的關係:每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個紙箱原型對象的內部指針。
基本用法:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
}
function SubType() {
this.subproperty = false;
}
// 子的原型是父的對象
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subproperty;
}
var instance = new SubType();
console.log(instance.getSuperValue()); // true
關係圖:
但實際上,SuperType也有自己的原型,就是Object,這個原型是默認的。
所有函數的默認原型都是Object的實例,因此默認原型都會包含一個內部指針,指向Object.prototype。
所以完整的關係圖應該是
使用原型能夠做到繼承,但實際中並不單獨使用原型鏈來實現繼承,原因如下:
1、對於不需要‘父親’的私有屬性的繼承:我們知道原型來創建對象,使得所有的實例都擁有這些共享的屬性和方法,我們在使用原型鏈來繼承最主要的是SubType的原型變爲SuperType的實例對象,那麼本來是Super實例私有的屬性property,且處於SubType的原型中成爲SubType實例的共享屬性。
2、對於需要‘父親‘私有屬性的繼承:同一,我們知道會繼承父親的私有屬性,但我們無法通過傳入參數到’父親‘的構造函數來實現屬性特有值的目的。
鑑於以上我們開始使用第二種繼承方式。
二、借用構造函數(僞造對象、經典繼承)
基本思想:在子類型構造函數的內部調用超類型構造函數
function SuperType() {
this.colors = ['red', 'yellow', 'black'];
}
SuperType.prototype.getColor = function () {
return this.colors;
}
function SubType() {
// 繼承了SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push('pink'); // ['red', 'yellow', 'black','pink']
console.log(instance1.colors);
var instance2 = new SubType();
console.log(instance2.colors); // ['red', 'yellow', 'black']
console.log(instance2 instanceof SuperType); // false
console.log(instance2.getColor()); // instance2.getColor is not a function
此方法是在子類型中調用了超(父)類型的構造函數,使構造函數中的屬性初始化了。
繼承的是超類型中構造函數中的屬性,如上繼承了colors屬性,但沒有繼承SuperType原型中的getcolor方法。
使用此方法,我們還可以傳遞參數對屬性進行初始化
function SuperType(age) {
this.age=age;
}
function SubType() {
// 繼承了SuperType
SuperType.call(this,18);
}
var instance1 = new SubType();
console.log(instance1.age); // 18
如果需要確保SuperType構造函數不會重寫子類型的屬性,可以在調用超類型構造函數後,再添加應該在子類型定義的屬性。
缺點:
1、超類型的原型不可見
2、所有屬性方法都必須寫在構造函數中,所有類型都只能使用構造函數模式創建
三、組合繼承(僞經典繼承)
將原型鏈和借用構造函數的技術組合到一塊。
思想:使用原型鏈實現對原型屬性和方法的繼承,借用構造函數實現對實例屬性的繼承。
function SuperType(age) {
this.age = age;
}
SuperType.prototype.getAge = function () {
return this.age;
}
function SubType(age) {
// 繼承了SuperType
SuperType.call(this, age);
}
SubType.prototype = new SuperType(20);
var instance1 = new SubType(18);
console.log(instance1.age); // 18
console.log(instance1.getAge()); // 18
console.log(instance1.__proto__.age); // 20
var instance2 = new SubType(17);
instance2.__proto__.age=55;
console.log(instance1.__proto__.age); // 55
console.log(instance2.__proto__.age); // 55
我們可以看到,實際上instance1和instance2的原型上仍然存在屬於SuperType的實例屬性的屬性。只是instance1和instance2有了各自的age屬性,不會再往原型上找。
instanceof和isPrototypeOf()也能夠用於識別基於組合繼承創建的對象。
組合繼承避免了原型鏈和借用構造函數的缺陷並融合了兩者的有點,成爲js中最常用的繼承模式。
四、原型式繼承
思想:藉助原型可以基於已有的對象創建新的對象,同時還不必因此創建自定義類型。
function object(o) {
function F() {
};
F.prototype = o;
return new F();
}
var person = {
name: 'linda',
friends: ['lily', 'shirley']
};
var antherPerson = object(person);
antherPerson.friends.push('tom');
console.log(antherPerson.name); // linda
console.log(antherPerson.friends); // ['lily', 'shirley', 'tom]
這個方法和原型方法原理一樣,只不過把子類型的原型設置成超類型的實例對象包含在方法object中。
ECMAScript5中新增了object.create()方法來規範原型式繼承(作用與上述object函數作用類似),第一個參數是想要繼承的超類型的實例對象,第二個參數是子類型所具有的屬性。
var person={
name:'lily',
age:12
}
var anotherPerson=Object.create(person,{name:{value:'linda'}});
console.log(anotherPerson.name); // 'linda'
第二個參數的寫法必須如上的格式。
支持Object.create()方法的瀏覽器有ie9+,Firefox4.+、Safari5+、Opera12+和Chrome
五、寄生式繼承
思想:與寄生構造函數和工廠模式類似,即創建一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最後再像真的是它做了所有工作一樣返回對象。
代碼:
function object(o){
function F(){}
F.prototype=o;
return new F();
}
var person = {
name: 'lily',
age: 12
}
function createAnotherPerson(original){
var clone=object(original);
clone.sayHi=function(){
console.log('hi');
}
return clone;
}
var anotherPerson =createAnotherPerson(person);
anotherPerson.sayHi(); // 'hi'
console.log(anotherPerson.name); // 'linda'
六、寄生組合式繼承
組合繼承是js最常用的繼承模式,可以結合不同的模式的優點,但組合繼承,每次都會調用兩次超類型構造函數:一次是在創建子類型原型的時候,另一次是在子類型構造函數內部。
上述造成的結果是子類型實例中有兩組超類型的構造函數中定義的屬性,一組在子類型的實例中,一組在子類型實例的原型中。寄生組合式繼承可以解決上述缺點。
function Super(name) {
this.name = name;
}
Super.prototype.sayName = function () {
console.log(this.name);
}
function Sub(age) {
Super.call(this, 'linda');
this.age = age;
}
Sub.prototype = new Super();
Sub.constructor = Sub;
var type=new Sub();
type.sayName(); // 'linda'
console.log(type.name); // 'linda'
console.log(type.__proto__.name); // undefined
思想:借用構造函數來繼承屬性,借用原型鏈來繼承方法。即繼承超類型的原型,然後再將結果指定給子類型的原型。
封裝一下即:
function Super(name) {
this.name = name;
}
Super.prototype.sayName = function () {
console.log(this.name);
}
function Sub(age) {
Super.call(this, 'linda');
this.age = age;
}
function object(o) {
function F() {
}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype); // 創建對象
prototype.constructor = subType; // 增強對象
subType.prototype = prototype; // 指定對象
}
inheritPrototype(Sub, Super);
var type = new Sub();
type.sayName(); // 'linda'
console.log(type.name); // 'linda'
console.log(type.__proto__.name); // undefined
這個模式的優點體現在
1、只調用了一次Super構造函數,高效率
2、避免了在Sub.prototype上面創建不必要的多餘的屬性
3、原型鏈保持不變
開發人員普遍認爲寄生組合式繼承是引用類型最理想的繼承範式