三、繼承
許多OO
語言都支持兩種繼承方式:接口繼承和實現繼承。接口繼承只繼承方法簽名,而實現繼承則繼承實際方法。由于函數沒有簽名,在ECMAScript
中無法實現接口繼承,只支持實現繼承,而且其實現繼承主要是依靠原型鏈來實現的。
3.1 原型鏈(很少單獨使用)
原型鏈的基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。簡單回顧一下構造函數、原型和實例的關系:每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。如果我們讓原型對象等于另一個類型的實例,那么就實現了繼承。下面通過例子說明:
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//inherit from SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
說明:繼承的圖解如下:
如圖所示,這里讓
SubType
原型指向一個SuperType
實例實現繼承,本質上是重寫了原型對象,代之以一個新類型的實例。要注意instance.constructor
現在指向的是SuperType
,這是因為原來SubType
中的constructor
被重寫的緣故(實際上,不是SubType
的原型的constructor
屬性被重寫了,而是SubType
的原型指向了另一個對象——SuperType
的原型,而這個原型對象的constructor
屬性指向的是SuperType
)。在調用方法或屬性時和前面講的一樣,一層層的搜索,直到最后。
3.1.1 別忘記默認的原型
起始所有引用類型默認都繼承了Object
,而這個繼承也是通過原型鏈實現的,如下所示:
3.1.2 確定原型和實例的關系
可以通過兩種方式來確定原型和實例之間的關系。第一種是使用instanceof
操作符,只要用這個操作符來測試實例與原型鏈中出現過的構造函數,結果就會返回true
:
alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true
第二種方式是使用isPrototypeOf()
方法,只要是原型鏈中出現過的原型,都可以說是該原型鏈所派生的實例的原型,因此此方法會返回true
:
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true
3.1.3 謹慎地定義方法
子類型有時候需要重寫超類型中的某個方法,或者添加超類型中不存在的某個方法。但不管這樣,給原型鏈添加方法的代碼一定要放在替換原型的語句之后,也就是原型鏈形成之后:
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;
};
//重寫方法
SubType.prototype.getSuperValue = function (){
return false;
};
var instance = new SubType();
alert(instance.getSuperValue()); //false
說明:起始很容易理解,因為我們在讓子類型的原型指向超類型的實例的時候,本質上來說是重寫了子類型的原型對象,既然原型對象都重寫了,當然在它之前添加的方法或重寫的方法都是無效的。同樣的,如果已經將子類型的原型指向超類型,那么在后面的代碼中如果又使用字面量添加新方法,則同樣會讓之前的原型鏈遭到破壞:
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//inherit from SuperType
SubType.prototype = new SuperType();
//使用字面量添加新方法,會導致上一行代碼無效
SubType.prototype = {
getSubValue : function (){
return this.subproperty;
},
someOtherMethod : function (){
return false;
}
};
var instance = new SubType();
alert(instance.getSuperValue()); //error!
3.1.4 原型鏈的問題
原型鏈最主要的問題來自包含引用類型的原型。通過例子說明:
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
}
//inherit from SuperType
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"
說明:我們在通過實例instance1
向父類屬性colors
中添加的元素對于所有子類實例來說都是共享的。原型鏈的第二個問題是:在創建子類型的實例時,不能向超類型的構造函數中傳遞參數,所以實踐中很少單獨使用原型鏈。
3.2 借用構造函數(很少單獨使用)
在解決原型中包含引用類型值所帶來問題的過程中,開發人員開始使用一種叫做借用構造函數的技術。這種技術的基本思想相當簡單,即在子類型構造函數的內部調用超類型構造函數。因為函數只不過是在特定環境中執行代碼的對象,因此通過使用apply()
和call()
方法也可以在(將來)新創建的對象上執行構造函數:
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
//inherit from 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"
說明:這里不再使用原型鏈中哪種將子類型原型指向超類型實例了,而是在子類型構造函數中調用父類型。這樣每個子類實例都有超類型中的引用屬性(colors
)副本了
3.2.1 傳遞參數
相對于原型鏈而言,借用構造函數有一個很大的優勢,即可以在子類型構造函數中向超類型構造函數傳遞參數:
function SuperType(name){
this.name = name;
}
function SubType(){
//繼承SuperType,同時還傳遞參數
SuperType.call(this, "Nicholas");
//實例屬性
this.age = 29;
}
var instance = new SubType();
alert(instance.name); //"Nicholas";
alert(instance.age); //29
說明:在SubType
構造函數內部調用SuperType
構造函數時,實際上是為SubType
的實例設置了name
屬性。為了確保SuperType
構造函數不會重寫子類型的屬性,可以在調用超類型構造函數后,再添加應該在子類型中定義的屬性。
3.2.2 借用構造函數的問題
如果僅僅是借用構造函數,那么也將無法避免構造函數存在的問題——方法都在構造函數中定義,因此函數復用就無從談起了。而且,在超類型的原型中定義的方法,對子類型而言也是不可見的(這不像原型鏈中子類原型指向超類型實例,這里并沒有構造原型鏈,所以不能訪問超類型原型方法)。考慮到這些問題,借用構造函數的技術也是很少單獨使用的。
3.3 組合繼承(最常見)
組合繼承有時候也叫偽經典繼承,指的是將原型鏈和借用構造函數的技術組合到一起,從而發揮二者之長的一種繼承模式。其思想是使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,既通過在原型上定義方法實現了函數復用,有能夠保證每個實例都有它自己的屬性。
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;
}
//將子類型原型指向超類型實例,實現原型鏈繼承
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,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
alert(SubType.prototype.constructor == SuperType.prototype.constructor);//true
alert(SuperType.isPrototypeOf(instance1));//false,這里instance1不是原型鏈派生的實例
alert(instance1 instanceof SuperType);//true
說明:組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,成為JS
中最常用的繼承模式。而且,instanceof
和isPrototypeOf()
也能夠用于識別基于組合模式繼承創建的對象。
3.4 原型式繼承
這種方法并沒有嚴格意義上的構造函數,就是借助原型可以基于已有對象創建新對象,同時還不必因此創建自定義類型。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
說明:此函數返回一個臨時類型的一個新實例。本質上講,object()
對傳入其中的對象執行了一次淺復制。看下面的例子:
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");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
說明:這種方法中將person
作為新對象的原型,于是所有新實例會共享引用屬性friends
,顯然這是不行的。ECMAScript 5
通過新增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);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
當然我們可以使用第二個參數進行修正,第二個參數與Obejct.defineProperties()
方法的第二個參數格式相同:每個屬性都是通過自己的描述符定義的。以這種方式指定的任何屬性都會覆蓋原型對象上的同名屬性。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
alert(anotherPerson.name); //"Greg"
可以看到,這里第二個參數定義的name
屬性就是新對象私有的。當然我們也可以再定義一個friends
屬性將原型person
中的同名屬性覆蓋掉。不要忘記:這種模式創建的對象包含引用類型值的屬性始終都會共享相應的值(除非覆蓋掉),就像原型模式一樣。
3.5 寄生式繼承
寄生式繼承是與原型式繼承緊密相關的一種思路。
function createAnother(original){
var clone = object(original);
clone.sayHi = function(){
console.log("Hi");
};
return clone;
}
說明:這種模式就是創建一個僅用于封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最后再像真地是它做了所有工作一樣返回對象。可以這樣使用:
var person = {
name : "Tom",
friends : ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi();//"Hi"
說明:在主要考慮對象而不是自定義類型和構造函數的情況下,寄生式繼承也是一種有用的模式。前面示例繼承模式中使用的object()
函數不是固定的,任何能夠返回新對象的函數都適用于此模式。
3.6 寄生組合式繼承(最有效方式)
前面講過,組合繼承是JS
最常見的繼承模式;不過也有不足。最大的問題就是不論在什么情況下,都會調用兩次超類型構造函數。
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);//第二次調用SuperType()
this.age = age;
}
//將子類型原型指向超類型實例,實現原型鏈繼承
SubType.prototype = new SuperType();//第一次調用SuperType()
SubType.prototype.sayAge = function(){
alert(this.age);
};
說明:第一次調用SuperType()
構造函數時,SubType.prototype
會得到兩個屬性:name
和colors
;它們都是SuperType
的實例屬性,只不過現在位于SubType
的原型中。當調用SubType
構造函數時,又會調用一次SuperType
構造函數,這一次i又在新對象上創建了實例屬性name
和colors
。于是,這兩個屬性就屏蔽了原型中的兩個同名屬性。如圖所示:
所謂寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。背后的基本思路是,不必為了指定子類型的原型而調用超類型的構造函數,我們所需要的無非就是超類型原型的一個部分而已。本質上,就是使用寄生式繼承來繼承超類型的原型,然后再將結果指定給子類型的原型。
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype);//創建對象
prototype.constructor = subType;//增強對象
subType.prototype = prototype;//指定對象
}
說明:這是寄生組合式繼承的最簡單的形式。這個函數接收兩個參數:子類型構造函數和超類型構造函數。在函數內部,第一步是創建超類型原型的一個副本。第二部是為創建的副本添加constructor
屬性,從而彌補因重寫原型而失去的默認的constructor
屬性。最后一步將新創建的對象(即副本)賦值給子類型的原型。
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);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
說明:此種模式不僅有組合模式的優點,同時只調用一次超類型構造函數,還能夠正常使用instanceof
和isPrototypeOf()
。