6.3、繼承
ES只支持實現繼承,而且其實現繼承主要是依靠原型鏈來實現的。
6.3.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;
};
var instance = new SubType();
alert(instance.getSubValue()); // true
alert(instance.getSuperValue()); // true
上面代碼的圖解如下所示:
調用 instance.getSuperValue() 會經歷三個搜索步驟:1)搜索實例;2)搜索 SubType.prototype;3)搜索 SuperType.prototype,最後一步纔會找到該方法。
1、別忘記默認的原型
所有類型默認都繼承了 Object,而這個繼承也是通過原型鏈實現的(所有函數的默認類型都是 Object 的實例)。具體如下圖所示:
2、確定原型和實例的關係
可以通過兩種方式來確定原型和實例之間的關係:
第一種:使用 instanceof 操作符,
// 由於原型鏈的關係,可以說 instance 是
// Object、SuperType、SubType中任何一個類型的實例
alert(instance instanceof Object); // true
alert(instance instanceof SuperType); // true
alert(instance instanceof SubType); // true
第二種:使用 isPrototypeOf() 方法,只要是原型鏈中出現過的原型,都可以說是該原型鏈所派生的實例的原型,因此 isPrototypeOf() 方法也會返回 true。
alert(Object.prototype.isPrototypeOf(instance)); // true
alert(SuperType.prototype.isPrototypeOf(instance)); // true
alert(SubType.prototype.isPrototypeOf(instance)); // true
3、慎重地定義方法
注意:給原型添加方法的代碼一定要放在替換原型的語句之後。
function SuperType() {
this.prototype = true;
}
SuperType.prototype.getSuperValue = function() {
return this.prototype;
};
function SubType() {
this.subprototype = false;
}
// 繼承 SuperType
SubType.prototype = new SuperType();
// 添加新方法
SubType.prototype.getSubValue = function() {
return this.subprototype;
};
// 重寫超類型中的方法
// 會屏蔽原來的那個方法
SubType.prototype.getSuperValue = function() {
return false;
};
var instance = new SubType();
alert(instance.getSubValue()); // false
alert(instance.getSuperValue()); // false
注意:在通過原型鏈實現繼承時,不能使用對象字面量創建原型方法。
function SuperType() {
this.prototype = true;
}
SuperType.prototype.getSuperValue = function() {
return this.prototype;
};
function SubType() {
this.subprototype = false;
}
// 繼承 SuperType
SubType.prototype = new SuperType();
// 使用字面量添加新方法,會導致上一行代碼無效
SubType.prototype = {
getSubValue : function() {
return this.subprototype;
},
someOtherMethod : function() {
return false;
}
};
var instance = new SubType();
alert(instance.getSuperValue()); // is not a function
4、原型鏈的問題
問題一:來自包含引用類型值的原型。
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}
function SubType() {
}
// 繼承了SuperType
// 繼承之後相當於 SubType.prototype.colors = ['red','blue','green'];
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push('black');
alert(instance1.colors); // "red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); // "red,blue,green,black"
var superType = new SuperType();
superType.colors.push('aa');
alert("superType colors are : " + superType.colors); // "red,blue,green,aa"
問題二:在創建子類型的實例時,不能向超類型的構造函數中傳遞參數。
6.3.2、借用構造函數
爲解決原型中包含引用類型值所帶來的問題,可以使用 借用構造函數 的技術來解決。即在子類型構造函數的內部調用超類型構造函數。
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 SubType();
alert(instance2.colors); // "red,blue,green"
1、傳遞參數
借用構造函數:可以在子類型構造函數中向超類型構造函數傳遞參數。
function SuperType(name) {
this.name = name;
}
function SubType() {
// 繼承了 SuperType,同時還傳遞了參數
// 調用超類型構造函數
SuperType.call(this, 'Nicholas');
// 相同的屬性名會覆蓋超類的同名屬性
// this.name = 'Greg';
// 實例屬性
this.age = 29;
}
var instance = new SubType();
alert(instance.name); // 'Nicholas'
alert(instance.age); // 29
2、借用構造函數的問題
無法避免構造函數模式中存在的問題——方法都在構造函數中定義,函數無法複用。
6.3.3、組合繼承
組合繼承(又稱爲 僞經典繼承):將原型鏈和借用構造函數的技術組合到一塊。使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。
function SuperType(name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
SuperType.prototype.sayName = function() {
alert(this.name);
};
function SubType(name, age) {
// 繼承超類 SuperType
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,green,blue,black"
instance1.sayName(); // 'Nicholas'
instance1.sayAge(); // 29
var instance2 = new SubType('Greg', 30);
alert(instance2.colors); // "red,green,blue"
instance2.sayName(); // 'Greg'
instance2.sayAge(); // 30
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,成爲JavaScript中最常用的繼承模式。
6.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'); // "Shelby,Court,Van,Rob"
alert(anotherPerson.name); // 'Greg'
var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');
alert(yetAnotherPerson.name); // 'Nicholas'
alert(person.friends); // "Shelby,Court,Van,Rob,Barbie"
ES5通過新增 Object.create() 方法規範了原型式繼承。這個方法接收兩個參數:一個用作新對象原型的對象和(可選的)一個爲新對象定義額外屬性的對象。在傳入一個參數的情況下,Object.create() 與 object() 方法的行爲相同。
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};
var anotherPerson = Object.create(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');
var yetAnotherPerson = Object.create(person);
alert(yetAnotherPerson.name); // 'Nicholas'
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');
alert(person.friends); // "Shelby,Court,Van,Rob,Barbie"
Object.create() 方法的第二個參數與 Object.defineProperties() 方法的第二個參數格式相同:每個屬性都是通過自己的描述符定義的。以這種方式指定的任何屬性都會覆蓋原型對象上的同名屬性。
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};
var anotherPerson = Object.create(person, {
name: {
value: 'Greg'
}
});
alert(anotherPerson.name); // 'Greg'
如果只想讓一個對象與另一個對象保持類似的情況下,原型繼承是完全可以勝任的。包含引用類型值的屬性始終都會共享相應的值,就像使用原型模式一樣。
6.3.5、寄生式繼承
寄生式繼承:即創建一個僅用於封裝過程的函數,該函數在內部以某種方式來增強對象,最後再像真地是它做了所有工作一樣返回對象。
function createAnother(original) {
// 通過調用函數創建一個新對象
var clone = Object(original);
// 以某種方式來增強這個對象
clone.sayHi = function() {
alert('Hi');
};
return clone; // 返回這個對象
}
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // 'Hi'
6.3.6、寄生組合式繼承
組合繼承最大的問題就是無論在什麼情況下,都會調用兩次超類型構造函數:一次是在創建子類型原型的時候,另一次是在子類型構造函數內部。
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
// SuperType的原型
SuperType.prototype.sayName = function() {
alert(this.name);
};
function SubType(name, age) {
// 第二次調用 SuperType()
SuperType.call(this, name);
this.age = age;
}
// 第一次調用 SuperType()
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
alert(this.age);
};
所謂寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。其背後的基本思路是:不必爲了指定類型的原型而調用超類型的構造函數,我們所需要的無非就是超類型原型的一個副本而已。
function inheritPrototype(subType, superType) {
// 創建對象
var prototype = Object(superType.prototype);
// 增強對象
prototype.constructor = subType;
// 指定對象
subType.prototype = prototype;
}
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);
};
參考文獻
[1]《JavaScript高級程序設計(第3版)》