javascript中的繼承
1、繼承分類
先來個整體印象。如圖所示,JS中繼承可以按照是否使用object函式(在下文中會提到),將繼承分成兩部分。
其中,原型鏈繼承和原型式繼承有一樣的優缺點,建構函式繼承與寄生式繼承也相互對應。寄生組合繼承於Object.create,同時優化了組合繼承,成為了完美的繼承方式。ES6 Class Extends的結果與寄生組哈繼承基本一致,但是實現方案又略有不同。
2、繼承方式
2.1、原型式繼承
核心:將父類的例項作為子類的原型
SubType.prototype = new SuperType()
// 所有涉及到原型鏈繼承的繼承方式都要修改子類建構函式的指向,否則子類例項的建構函式會指向SuperType
SubType .prototype.constructor = SubType;
優點:父類方法可以複用。
缺點:
父類的引用屬性會被所有子類例項共享
子類構建例項時不能向父類傳遞引數
2.2 建構函式繼承
核心:將父類建構函式的內容複製給了子類的建構函式。這是所有繼承中唯一一個不涉及到prototype的繼承。
SuperType.call(SubType);
優點:和原型鏈繼承完全反過來
父類的引用屬性不會被共享
子類構建例項時可以向父類傳遞引數
缺點:父類的方法不能複用,子類例項的方法每次都是單獨建立的。
2.3、組合繼承
核心:原型式繼承和建構函式繼承的組合,兼具了二者的優點。
function SuperType(){
this.name = 'parent';
this.arr = [1,2,3];
}
SuperType.prototype.say = function(){
console.log('this is parent')
}
function SubType(){
SuperType.call(this); // 第二次呼叫SuperType
}
SubType.prototype = new SuperType() // 第一次呼叫SuperType
優點:
父類的方法可以被複用
父類的引用屬性不會被共享
子類構建例項時可以向父類傳遞引數
缺點:呼叫了兩次父類的建構函式,第一次給子類的原型添加了父類的name,arr屬性,第二次又給子類的建構函式添加了父類的name,arr屬性,從而覆蓋了子類原型中的同名引數。這種被覆蓋的情況造成了效能上的浪費。
2.4、原型式繼承
核心:原型式繼承的object方法本質上是對引數物件的一個淺複製。
有點:父類方法可以複用。
缺點:
父類的引用屬性會被所有子類例項共享
子類構建例項時不能向父類傳遞引數
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var person = {
name: "peiqi",
friends: ["hello","rookie","van"]
}
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends);
ECMAScript5通過新增Object.create()方法規範化了原型式繼承。這個方法接收兩個引數:一個用作新物件原型的物件和(可選的)一個為新物件定義額外屬性的物件。在傳入一個引數的情況下,Object.create()與object()方法的行為相同。
所以上文中程式碼可以轉變為:
var yetAnotherPerson = object(person); => var yetAnotherPerson = Object.create(person);
2.5 寄生式繼承
核心:使用原型式繼承獲得一個目標物件的淺複製,然後增強這個淺複製的能力。
優缺點,僅提供一種思路,沒什麼優點。
function createAnother(original){
var clone = object(original); //通過呼叫函式建立一個新物件
clone.sayHi = function(){ // 以某種方式來增強這個物件
alert("hi");
};
return clone; // 返回這個物件
}
var parson = {
name: "peiqi",
friends: ["hello","rookie","van"]
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // hi
2.6、寄生組合繼承
剛才說到組合繼承有一個會兩次呼叫父類的建構函式造成浪費的缺點,寄生組合繼承就可以解決這個問題。
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);
}
優缺點:這是一種完美的繼承方式。
2.7、ES6 Class extends
核心:ES6繼承的結果和寄生組合繼承相似,本質上,ES6繼承是一種語法糖。但是寄生組合繼承是先建立子類例項this物件,然後在對其增強;而ES6先將父類例項物件的屬性和方法,加到this上面(所以必須先呼叫super方法),然後再用子類的建構函式修改this。
class A{}
class B extends A{
constructor(){
super();
}
}
ES6實現繼承的具體原理:
class A {
}
class B {
}
Object.setPrototypeOf = function(obj, proto){
obj.\__proto__ = proto;
return obj;
}
// B 的例項繼承A的例項
Object.setPrototypeOf(B.prototype, A.prototype);
// B 繼承A的靜態屬性
Object.setPrototypeOf(B, A);
ES6繼承ES5繼承的異同:
相同點:本質上ES6繼承是ES5繼承的語法糖。
不同點:
ES6繼承中子類的建構函式的原型鏈指向父類的建構函式,ES5中使用的建構函式複製,沒有原型鏈指向。
ES6子類例項的構建,基於父類例項,ES5中不是。
3、總結
ES6 Class extends是ES5繼承的語法糖
JS的繼承除了建構函式繼承之外都基於原型鏈構建的
可以用寄生組合繼承實現ES6 Class extends,但是還是會有細微的差別.