JavaScript實現繼承的幾種重要範式
一 原型鏈
1. 代碼示例
function SuperType() { this.superProperty = true; } SuperType.prototype.getSuperValue = function() { return this.superProperty; } function SubType() { this.subProperty = false; } SubType.prototype = new SuperType(); //將 SuperType類型的實例 作為 SubType類型的 原型對象, 這樣就重寫了SubType的原型對象(沒有使用SubType默認的原型對象), 實現了繼承。 SubType.prototype.getSubValue = function() { return this.subProperty; } const subTypeInstance = new SubType(); console.log(subTypeInstance.getSuperValue());//true
詳見我的另一篇博客《原型與原型鏈》 的"二、實現繼承的主要範式:原型鏈
"。
二、 借用構造函數(經典繼承)
1.代碼示例
function SuperType() { this.colors = [‘red‘, ‘blue‘, ‘green‘]; } function SubType() { SuperType.call(this);//在子類型構造函數內部調用超類型構造函數,繼承了SuperType } var subTypeInstance1 = new SubType(); subTypeInstance1.colors.push(‘yellow‘); console.log(subTypeInstance1);//[‘red‘, ‘blue‘, ‘green‘,‘yellow‘] var subTypeInstance2 = new SubType(); console.log(subTypeInstance2.colors);//[‘red‘, ‘blue‘, ‘green‘]
基本思想就是在子類型構造函數內部調用超類型構造函數。
2. 優點
(1)子類型每個實例都會繼承一份獨立的超類型屬性副本
通過使用call方法(或apply),實際上是在未來新創建子類型實例時當場調用了超類型的構造函數,也就是在初始化子類型實例時才把超類型的屬性添加到子類型實例上。那麽子類型的每個實例都會擁有一份獨立的超類型屬性副本。 這樣不同的子類型實例對同一個繼承來的屬性進行修改(例如對數組屬性進行push),也不會互相影響。
(2)可以在子類型構造函數中向超類型構造函數傳遞參數
function SuperType(name) { this.name = name; } function SubType(name) { SuperType.call(this,name); } var subTypeInstance1 = new SubType(‘Bonnie‘); console.log(subTypeInstance1.name);//"Bonnie" var subTypeInstance2 = new SubType(‘Summer‘); console.log(subTypeInstance2.name);//"Summer"
3. 缺點
(1)不能做到函數復用:無法避免構造函數模式的問題——使用構造函數模式創建的每個實例都包含著各自獨有的同名函數,故函數復用無從談起。
(2)子類型創建方式限制:而在超類型的原型中定義的方法,對子類而言是不可見的,所以所有子類型都只能通過構造函數模式創建。
三、 組合繼承(偽經典繼承)
1. 代碼示例
function SuperType(name) {
this.name = name;
this.colors = [‘red‘, ‘blue‘, ‘green‘];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
//繼承屬性
SuperType.call(this, name); //第二次調用超類型構造函數SuperType
//自己的屬性
this.age = age;
}
//繼承方法:SubType.prototype也會得到繼承的屬性,不過會被上述構造函數中call方法繼承的屬性作為實例屬性覆蓋掉。
SubType.prototype = new SuperType(); //第一次調用超類型構造函數SuperType
SubType.prototype.constructor = SubType;//重寫prototype會割裂子類型原型與子類型構造函數的關系,故要加上這麽一句
//自己的方法
SubType.sayAge = function() {
console.log(this.age);
}
將原型鏈和借用構造函數組合起來:使用原型鏈實現對超類型原型上的方法和屬性的繼承,使用借用構造函數實現對超類型實例屬性和方法的繼承。一般超類型原型上就只有方法,超類型實例上只有屬性(即 組合使用構造函數模式和原型模式,參見3.4)。這樣一來,該方式就是:用原型鏈實現對超類型原型上方法的繼承,用借用構造函數實現對超類型實例上屬性的繼承。
2. 優點
組合繼承用 原型鏈實現對 超類型原型上方法的繼承,用 借用構造函數實現對 超類型實例上屬性的繼承。這樣可以讓子類型的不同實例既分別擁有獨立的屬性(尤其是引用類型屬性,如colors數組),又可以共享相同的方法。
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,是 JavaScript中最常用的繼承模式。
3. 缺點
無論在什麽情況下,都會調用兩次超類型構造函數:一次是在創建子類型原型時,一次是在子類型構造函數內部。這樣的話,雖然子類型會包含超類型的全部屬性,但是對於超類型的實例屬性而言,調用子類型的構造函數時會重寫一遍這些實例屬性。
四、 原型式繼承
1. 代碼示例
function object(o) {
function F(){};//先構造一個臨時性的構造函數
F.prototype = o;//將傳入的對象作為該構造函數的原型
return new F();//返回臨時類型的新實例
}
//使用
var person = {
name:‘Bonnie‘,
friends: [‘Summer‘, ‘Spring‘]
}
var person1 = object(person);
person1.name = ‘Huiyun‘;
person1.friends.push(‘Tony‘);
var person2 = object(person);
person2.name = ‘Huiyun‘;
person2.friends.push(‘Joy‘);
console.log(person1.friends);//["Summer", "Spring", "Tony", "Joy"]
console.log(person2.friends);//["Summer", "Spring", "Tony", "Joy"]
console.log(person.friends);//["Summer", "Spring", "Tony", "Joy"]
思想: 借助原型可以基於已有的對象(而非類型)創建新對象(而非創建自定義類型)。其實,object對傳入其中的對象執行了一次 淺復制。
2. 優點
可以基於已有的對象創建新對象,而且還不必創建新類型。適於基於已有對象加以修改得到另一個對象。
在只想讓一個對象與另一個對象保持類似,又不想興師動眾創建構造函數的情況下,原型式繼承完全可以勝任。
延伸:ES6的Object.create()
該方法規範化了原型式繼承,在只傳入一個參數的情況下,和上述object達到的效果相同。該方法的第二個參數為新對象額外屬性組成的對象,也就是簡化了上述object的後續用法。
語法:
Object.create(protoObj, [newPropertiesObj])
參數:
- protoObj: 新創建對象的原型對象
- newPropertiesObj:可選。 要添加(或重寫)到新對象上的可枚舉的實例屬性的屬性描述符及其名稱組成的對象(與Object.defineProperties()的第二個參數相同)。
newPropertyiesObj語法:
{
prop1Name:{
value: valueConent,
writable: false(default)/true
enumerable: false(default)/true
configuragle: false(default)/true
},
prop2Name: {
...
},
...
}
詳見https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties
返回值: 一個帶有指定的原型對象屬性和自己新添加的實例屬性的對象
Eg:
var person = {
name: ‘Bonnie‘,
friends: [‘Summer‘,‘Spring‘]
}
var person1 = Object.create(person, {
name:{
value:‘Huiyun‘
}
});
console.log(person1);//{name: "Huiyun"}
console.log(person1.friends);// ["Summer", "Spring"]
3.缺點
引用類型屬性共享: 該方法基於已有對象創建新對象,但是對新對象修改引用類型屬性,已有的基礎對象也會受到修改,基於基礎對象的其他對象也會受到修改。
五、 寄生式繼承
1. 代碼示例
function createAnother(original) {
var clone = object(original);//此處運用了4.4中的objec函數,也可以使用Object.create(original)
clone.sayHi = function() {
console.log(‘Hi‘);
}
return clone;
}
思想:與創建對象的寄生構造函數模式和工廠模式類似,即創建了一個僅用於封裝繼承過程的函數,並在該函數內部以某種方式來增強對象,最後再像真地是它自己做了所有工作一樣返回對象。
2. 優點
在主要考慮基於某個對象而非考慮自定義類型和構造函數的情況下,寄生式繼承也是一種有用的模式。該模式可以基於已有對象創建添加了函數的新對象。
3. 缺點
(1)不能做到函數復用:不能做到對新添加函數進行函數復用,這樣會降低效率。該缺點與 借用構造函數繼承類似。
(2)引用類型屬性共享:該方法也是基於已有對象創建新對象,但是對新對象修改引用類型屬性,已有的基礎對象也會受到修改,基於基礎對象的其他對象也會受到修改。該缺點 與原型式繼承 一樣。
六、 寄生組合式繼承
1. 代碼示例
//其實是寄生式繼承的一種應用:以SuperType.prototype為基礎對象,創建SubType.prototype對象。這個SubType.prototype對象是SuperType.prototype的淺復制,同時SubType.prototype對象上又增添了額外的屬性constructor指向SubType。
function inheritPrototype(SubType, SuperType) {
var prototype = Object.create(SuperType.prototype);
prototype.constructor = SubType;
Subtype.prototype = prototype;
}
//應用
function SuperType(name) {
this.name = name;
this.colors = [‘red‘, ‘blue‘];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
//子類型實例通過借用構造函數繼承來獲取超類型實例上的屬性:
function Subtype(name, age) {
SuperType.call(this,name);
this.age = age;
}
//子類型的原型通過寄生式繼承來獲取超類型原型上的方法:
inheritPrototype(SubType,SuperType);
//給子類型原型添加自己的方法
SubType.prototype.sayAge = function() {
console.log(this.age);
}
思想: 子類型的實例通過借用構造函數來獲取超類型實例上的屬性,子類型的原型通過寄生式繼承來獲取超類型原型上的方法(此處寄生式繼承是指子類型原型對超類型原型進行淺復制)。也就是說子類型通過借用構造函數繼承屬性,通過寄生式繼承來繼承方法。 和組合式繼承相比,該寄生組合式繼承不必為了指定子類型的原型而調用超類型的構造函數,只是使用了超類型原型的一個副本;而只有在指定子類型的實例屬性時調用了超類型的構造函數(借用構造函數繼承);這樣該方法就只調用了一次超類型的構造函數。
2. 優點
(1) 屬性獨立、方法共享:擁有組合式繼承的所有優點:分別擁有獨立的屬性(尤其是引用類型屬性,如colors數組),又可以共享相同的方法。
(2) 高效率:避免了組合式繼承調用兩次超類型構造函數的缺點,只調用一次超類型構造函數,具有高效率。
(3) 原型鏈不變:能夠正常使用instanceof和isPrototypeOf()
該寄生組合式繼承是最理想的繼承範式。
七、Es6的Class對繼承的實現*
參考資料
《JavaScirpt高級程序設計》6.3
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties
JavaScript實現繼承的幾種重要範式