上一篇講到j(luò)s實(shí)現(xiàn)對(duì)類對(duì)封裝窄俏,本篇介紹類的繼承约谈,多態(tài)指巡。
類的繼承
- 類式繼承
類式繼承方式是將父類的實(shí)例賦在子類的原型上實(shí)現(xiàn)繼承的效果扒吁。
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.getSuperValue()); //true
這個(gè)例子中的 SuperType 構(gòu)造函數(shù)定義了一個(gè) colors 屬性火鼻,該屬性包含一個(gè)數(shù)組(引用類型值)。 SuperType 的每個(gè)實(shí)例都會(huì)有各自包含自己數(shù)組的 colors 屬性雕崩。當(dāng) SubType 通過(guò)原型鏈繼承了 SuperType 之后魁索,SubType.prototype 就變成了 SuperType 的一個(gè)實(shí)例,因此它也擁有了一個(gè)它自 己的 colors 屬性——就跟專門創(chuàng)建了一個(gè) SubType.prototype.colors 屬性一樣晨逝。但結(jié)果是什么 呢?結(jié)果是 SubType 的所有實(shí)例都會(huì)共享這一個(gè) colors 屬性蛾默。而我們對(duì) instance1.colors 的修改 能夠通過(guò) instance2.colors 反映出來(lái),就已經(jīng)充分證實(shí)了這一點(diǎn)捉貌。
原型鏈的第二個(gè)問(wèn)題是:在創(chuàng)建子類型的實(shí)例時(shí)支鸡,不能向超類型的構(gòu)造函數(shù)中傳遞參數(shù)冬念。實(shí)際上, 應(yīng)該說(shuō)是沒(méi)有辦法在不影響所有對(duì)象實(shí)例的情況下牧挣,給超類型的構(gòu)造函數(shù)傳遞參數(shù)急前。有鑒于此,再加上 前面剛剛討論過(guò)的由于原型中包含引用類型值所帶來(lái)的問(wèn)題瀑构,實(shí)踐中很少會(huì)單獨(dú)使用原型鏈裆针。
- 構(gòu)造函數(shù)繼承
在解決原型中包含引用類型值所帶來(lái)問(wèn)題的過(guò)程中,開(kāi)發(fā)人員開(kāi)始使用一種叫做借用構(gòu)造函數(shù) (constructor stealing)的技術(shù)(有時(shí)候也叫做偽造對(duì)象或經(jīng)典繼承)寺晌。這種技術(shù)的基本思想相當(dāng)簡(jiǎn)單世吨,即
在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型構(gòu)造函數(shù)。別忘了呻征,函數(shù)只不過(guò)是在特定環(huán)境中執(zhí)行代碼的對(duì)象耘婚, 因此通過(guò)使用 apply()和 call()方法也可以在(將來(lái))新創(chuàng)建的對(duì)象上執(zhí)行構(gòu)造函數(shù)
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"
代碼中加粗的那一行代碼“借調(diào)”了超類型的構(gòu)造函數(shù)。通過(guò)使用 call()方法(或 apply()方法 也可以)陆赋,我們實(shí)際上是在(未來(lái)將要)新創(chuàng)建的 SubType 實(shí)例的環(huán)境下調(diào)用了 SuperType 構(gòu)造函數(shù)沐祷。 這樣一來(lái),就會(huì)在新 SubType 對(duì)象上執(zhí)行 SuperType()函數(shù)中定義的所有對(duì)象初始化代碼攒岛。結(jié)果赖临, SubType 的每個(gè)實(shí)例就都會(huì)具有自己的 colors 屬性的副本了。
- 組合繼承
組合繼承(combination inheritance)灾锯,有時(shí)候也叫做偽經(jīng)典繼承兢榨,指的是將原型鏈和借用構(gòu)造函數(shù)的 技術(shù)組合到一塊,從而發(fā)揮二者之長(zhǎng)的一種繼承模式挠进。其背后的思路是使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方 法的繼承色乾,而通過(guò)借用構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承誊册。這樣领突,既通過(guò)在原型上定義方法實(shí)現(xiàn)了函數(shù) 復(fù)用,又能夠保證每個(gè)實(shí)例都有它自己的屬性案怯。下面來(lái)看一個(gè)例子君旦。
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
在這個(gè)例子中,SuperType 構(gòu)造函數(shù)定義了兩個(gè)屬性:name 和 colors嘲碱。SuperType 的原型定義 了一個(gè)方法 sayName()金砍。SubType 構(gòu)造函數(shù)在調(diào)用 SuperType 構(gòu)造函數(shù)時(shí)傳入了 name 參數(shù),緊接著 又定義了它自己的屬性 age麦锯。然后恕稠,將 SuperType 的實(shí)例賦值給 SubType 的原型,然后又在該新原型 上定義了方法 sayAge()扶欣。這樣一來(lái)鹅巍,就可以讓兩個(gè)不同的 SubType 實(shí)例既分別擁有自己屬性——包 括 colors 屬性千扶,又可以使用相同的方法了。
組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷骆捧,融合了它們的優(yōu)點(diǎn)澎羞,成為 JavaScript 中最常用的繼 承模式。而且敛苇,instanceof 和 isPrototypeOf()也能夠用于識(shí)別基于組合繼承創(chuàng)建的對(duì)象妆绞。
- 原型式繼承
道格拉斯·克羅克福德在 2006 年寫(xiě)了一篇文章,題為 Prototypal Inheritance in JavaScript (JavaScript 10 中的原型式繼承)枫攀。在這篇文章中括饶,他介紹了一種實(shí)現(xiàn)繼承的方法,這種方法并沒(méi)有使用嚴(yán)格意義上的 構(gòu)造函數(shù)来涨。他的想法是借助原型可以基于已有的對(duì)象創(chuàng)建新對(duì)象巷帝,同時(shí)還不必因此創(chuàng)建自定義類型。為 了達(dá)到這個(gè)目的扫夜,他給出了如下函數(shù)楞泼。
function object(o){
function F(){}
F.prototype = o;
return new F(); 12
}
在 object()函數(shù)內(nèi)部,先創(chuàng)建了一個(gè)臨時(shí)性的構(gòu)造函數(shù)笤闯,然后將傳入的對(duì)象作為這個(gè)構(gòu)造函數(shù)的 原型堕阔,最后返回了這個(gè)臨時(shí)類型的一個(gè)新實(shí)例。從本質(zhì)上講颗味,object()對(duì)傳入其中的對(duì)象執(zhí)行了一次淺復(fù)制超陆。來(lái)看下面的例子。
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"
克羅克福德主張的這種原型式繼承浦马,要求你必須有一個(gè)對(duì)象可以作為另一個(gè)對(duì)象的基礎(chǔ)时呀。如果有這么 一個(gè)對(duì)象的話,可以把它傳遞給 object()函數(shù)晶默,然后再根據(jù)具體需求對(duì)得到的對(duì)象加以修改即可谨娜。在這 個(gè)例子中,可以作為另一個(gè)對(duì)象基礎(chǔ)的是 person 對(duì)象磺陡,于是我們把它傳入到 object()函數(shù)中趴梢,然后該 函數(shù)就會(huì)返回一個(gè)新對(duì)象。這個(gè)新對(duì)象將 person 作為原型币他,所以它的原型中就包含一個(gè)基本類型值屬性 和一個(gè)引用類型值屬性坞靶。這意味著 person.friends 不僅屬于 person 所有,而且也會(huì)被 anotherPerson 以及 yetAnotherPerson 共享蝴悉。實(shí)際上彰阴,這就相當(dāng)于又創(chuàng)建了 person 對(duì)象的兩個(gè)副本。
ECMAScript 5 通過(guò)新增 Object.create()方法規(guī)范化了原型式繼承拍冠。這個(gè)方法接收兩個(gè)參數(shù):一 個(gè)用作新對(duì)象原型的對(duì)象和(可選的)一個(gè)為新對(duì)象定義額外屬性的對(duì)象尿这。在傳入一個(gè)參數(shù)的情況下廉丽, 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"
Object.create()方法的第二個(gè)參數(shù)與 Object.defineProperties()方法的第二個(gè)參數(shù)格式相
同:每個(gè)屬性都是通過(guò)自己的描述符定義的妻味。以這種方式指定的任何屬性都會(huì)覆蓋原型對(duì)象上的同名屬
性正压。例如:
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
alert(anotherPerson.name); //"Greg"
支持 Object.create()方法的瀏覽器有 IE9+、Firefox 4+责球、Safari 5+焦履、Opera 12+和 Chrome。
在沒(méi)有必要興師動(dòng)眾地創(chuàng)建構(gòu)造函數(shù)雏逾,而只想讓一個(gè)對(duì)象與另一個(gè)對(duì)象保持類似的情況下嘉裤,原型式 繼承是完全可以勝任的。不過(guò)別忘了栖博,包含引用類型值的屬性始終都會(huì)共享相應(yīng)的值屑宠,就像使用原型模 式一樣。
- 寄生式繼承
寄生式(parasitic)繼承是與原型式繼承緊密相關(guān)的一種思路仇让,并且同樣也是由克羅克福德推而廣 之的典奉。寄生式繼承的思路與寄生構(gòu)造函數(shù)和工廠模式類似,即創(chuàng)建一個(gè)僅用于封裝繼承過(guò)程的函數(shù)丧叽,該 函數(shù)在內(nèi)部以某種方式來(lái)增強(qiáng)對(duì)象卫玖,最后再像真地是它做了所有工作一樣返回對(duì)象。以下代碼示范了寄 生式繼承模式踊淳。
function createAnother(original){ varclone=object(original); //通過(guò)調(diào)用函數(shù)創(chuàng)建一個(gè)新對(duì)象
}
clone.sayHi = function(){
alert("hi");
};
return clone;
//以某種方式來(lái)增強(qiáng)這個(gè)對(duì)象
//返回這個(gè)對(duì)象
在這個(gè)例子中假瞬,createAnother()函數(shù)接收了一個(gè)參數(shù),也就是將要作為新對(duì)象基礎(chǔ)的對(duì)象迂尝。然 后脱茉,把這個(gè)對(duì)象(original)傳遞給 object()函數(shù),將返回的結(jié)果賦值給 clone垄开。再為 clone 對(duì)象 添加一個(gè)新方法 sayHi()琴许,最后返回 clone 對(duì)象∷涤埽可以像下面這樣來(lái)使用 createAnother()函數(shù):
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
這個(gè)例子中的代碼基于 person 返回了一個(gè)新對(duì)象——anotherPerson虚吟。新對(duì)象不僅具有 person 的所有屬性和方法,而且還有自己的 sayHi()方法签财。
在主要考慮對(duì)象而不是自定義類型和構(gòu)造函數(shù)的情況下,寄生式繼承也是一種有用的模式偏塞。前面示 范繼承模式時(shí)使用的 object()函數(shù)不是必需的;任何能夠返回新對(duì)象的函數(shù)都適用于此模式唱蒸。
- 寄生組合式繼承
前面說(shuō)過(guò),組合繼承是 JavaScript 最常用的繼承模式;不過(guò)灸叼,它也有自己的不足神汹。組合繼承最大的 問(wèn)題就是無(wú)論什么情況下庆捺,都會(huì)調(diào)用兩次超類型構(gòu)造函數(shù):一次是在創(chuàng)建子類型原型的時(shí)候,另一次是 在子類型構(gòu)造函數(shù)內(nèi)部屁魏。沒(méi)錯(cuò)滔以,子類型最終會(huì)包含超類型對(duì)象的全部實(shí)例屬性,但我們不得不在調(diào)用子 類型構(gòu)造函數(shù)時(shí)重寫(xiě)這些屬性氓拼。再來(lái)看一看下面組合繼承的例子你画。
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);
};
加粗字體的行中是調(diào)用 SuperType 構(gòu)造函數(shù)的代碼。在第一次調(diào)用 SuperType 構(gòu)造函數(shù)時(shí)桃漾, SubType.prototype 會(huì)得到兩個(gè)屬性:name 和 colors;它們都是 SuperType 的實(shí)例屬性坏匪,只不過(guò) 現(xiàn)在位于 SubType 的原型中。當(dāng)調(diào)用 SubType 構(gòu)造函數(shù)時(shí)撬统,又會(huì)調(diào)用一次 SuperType 構(gòu)造函數(shù)适滓,這 一次又在新對(duì)象上創(chuàng)建了實(shí)例屬性 name 和 colors。于是恋追,這兩個(gè)屬性就屏蔽了原型中的兩個(gè)同名屬 性凭迹。有兩組 name 和 colors 屬性:一組在實(shí)例上,一組在 SubType 原型中苦囱。這就是調(diào) 用兩次 SuperType 構(gòu)造函數(shù)的結(jié)果蕊苗。好在我們已經(jīng)找到了解決這個(gè)問(wèn)題方法——寄生組合式繼承。
所謂寄生組合式繼承沿彭,即通過(guò)借用構(gòu)造函數(shù)來(lái)繼承屬性朽砰,通過(guò)原型鏈的混成形式來(lái)繼承方法。其背 后的基本思路是:不必為了指定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù)喉刘,我們所需要的無(wú)非就是超類型 原型的一個(gè)副本而已瞧柔。本質(zhì)上,就是使用寄生式繼承來(lái)繼承超類型的原型睦裳,然后再將結(jié)果指定給子類型 的原型造锅。寄生組合式繼承的基本模式如下所示。
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype);//創(chuàng)建對(duì)象
prototype.constructor = subType; //增強(qiáng)對(duì)象
subType.prototype = prototype; //指定對(duì)象
}
這個(gè)示例中的 inheritPrototype()函數(shù)實(shí)現(xiàn)了寄生組合式繼承的最簡(jiǎn)單形式廉邑。這個(gè)函數(shù)接收兩 個(gè)參數(shù):子類型構(gòu)造函數(shù)和超類型構(gòu)造函數(shù)哥蔚。在函數(shù)內(nèi)部,第一步是創(chuàng)建超類型原型的一個(gè)副本蛛蒙。第二 步是為創(chuàng)建的副本添加 constructor 屬性糙箍,從而彌補(bǔ)因重寫(xiě)原型而失去的默認(rèn)的 constructor 屬性。 最后一步牵祟,將新創(chuàng)建的對(duì)象(即副本)賦值給子類型的原型深夯。這樣,我們就可以用調(diào)用 inherit- Prototype()函數(shù)的語(yǔ)句,去替換前面例子中為子類型原型賦值的語(yǔ)句了咕晋,例如:
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);
}
這個(gè)例子的高效率體現(xiàn)在它只調(diào)用了一次 SuperType 構(gòu)造函數(shù)雹拄,并且因此避免了在 SubType. prototype 上面創(chuàng)建不必要的、多余的屬性掌呜。與此同時(shí)滓玖,原型鏈還能保持不變;因此,還能夠正常使用 instanceof 和 isPrototypeOf()质蕉。開(kāi)發(fā)人員普遍認(rèn)為寄生組合式繼承是引用類型最理想的繼承范式势篡。
小結(jié)
ECMAScript 支持面向?qū)ο?OO)編程,但不使用類或者接饰剥。對(duì)象可以在代碼執(zhí)行過(guò)程中創(chuàng)建和 增強(qiáng)殊霞,因此具有動(dòng)態(tài)性而非嚴(yán)格定義的實(shí)體。在沒(méi)有類的情況下汰蓉,可以采用下列模式創(chuàng)建對(duì)象绷蹲。
- 類模式
使用簡(jiǎn)單的函數(shù)創(chuàng)建對(duì)象,為對(duì)象添加屬性和方法顾孽,然后返回對(duì)象祝钢。這個(gè)模式后來(lái) 被構(gòu)造函數(shù)模式所取代。
- 構(gòu)造函數(shù)模式.
可以創(chuàng)建自定義引用類型若厚,可以像創(chuàng)建內(nèi)置對(duì)象實(shí)例一樣使用 new 操作符拦英。不 過(guò),構(gòu)造函數(shù)模式也有缺點(diǎn)测秸,即它的每個(gè)成員都無(wú)法得到復(fù)用疤估,包括函數(shù)。由于函數(shù)可以不局 限于任何對(duì)象(即與對(duì)象具有松散耦合的特點(diǎn))霎冯,因此沒(méi)有理由不在多個(gè)對(duì)象間共享函數(shù)铃拇。
- 原型模式.
使用構(gòu)造函數(shù)的prototype屬性來(lái)指定那些應(yīng)該共享的屬性和方法。組合使用構(gòu)造 函數(shù)模式和原型模式時(shí)沈撞,使用構(gòu)造函數(shù)定義實(shí)例屬性慷荔,而使用原型定義共享的屬性和方法。 JavaScript 主要通過(guò)原型鏈實(shí)現(xiàn)繼承缠俺。原型鏈的構(gòu)建是通過(guò)將一個(gè)類型的實(shí)例賦值給另一個(gè)構(gòu)造函
數(shù)的原型實(shí)現(xiàn)的显晶。這樣,子類型就能夠訪問(wèn)超類型的所有屬性和方法壹士,這一點(diǎn)與基于類的繼承很相似磷雇。 原型鏈的問(wèn)題是對(duì)象實(shí)例共享所有繼承的屬性和方法,因此不適宜單獨(dú)使用墓卦。解決這個(gè)問(wèn)題的技術(shù)是借 用構(gòu)造函數(shù)倦春,即在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型構(gòu)造函數(shù)。這樣就可以做到每個(gè)實(shí)例都具有自己的 屬性落剪,同時(shí)還能保證只使用構(gòu)造函數(shù)模式來(lái)定義類型睁本。使用最多的繼承模式是組合繼承,這種模式使用 原型鏈繼承共享的屬性和方法忠怖,而通過(guò)借用構(gòu)造函數(shù)繼承實(shí)例屬性呢堰。
此外,還存在下列可供選擇的繼承模式凡泣。
- 原型式繼承
可以在不必預(yù)先定義構(gòu)造函數(shù)的情況下實(shí)現(xiàn)繼承枉疼,其本質(zhì)是執(zhí)行對(duì)給定對(duì)象的淺
復(fù)制。而復(fù)制得到的副本還可以得到進(jìn)一步改造鞋拟。
- 寄生式繼承
與原型式繼承非常相似骂维,也是基于某個(gè)對(duì)象或某些信息創(chuàng)建一個(gè)對(duì)象,然后增強(qiáng)
對(duì)象贺纲,最后返回對(duì)象航闺。為了解決組合繼承模式由于多次調(diào)用超類型構(gòu)造函數(shù)而導(dǎo)致的低效率問(wèn)題,可以將這個(gè)模式與組合繼承一起使用猴誊。
- 寄生組合式繼承
集寄生式繼承和組合繼承的優(yōu)點(diǎn)與一身潦刃,是實(shí)現(xiàn)基于類型繼承的最有效方式