我們?cè)?a href="http://www.reibang.com/p/375b0646cbd1" target="_blank">對(duì)象創(chuàng)建模式中討論過(guò)盛泡,對(duì)象創(chuàng)建的模式就是定義對(duì)象模板的方式钝满。有了模板以后,我們就可以輕松地創(chuàng)建多個(gè)結(jié)構(gòu)相同的對(duì)象了绞绒。
繼承就是對(duì)象創(chuàng)建模式的擴(kuò)展,我們需要在舊模板的基礎(chǔ)上榕暇,添加新的特性蓬衡,使之成為一個(gè)新的模板喻杈。
為了更加迎合程序員的“直覺(jué)”,本篇文章有時(shí)使用“父類”來(lái)指代“父對(duì)象的模板”狰晚。但是讀者應(yīng)該明確:JavaScript沒(méi)有類系統(tǒng)筒饰。
1. 純?cè)玩溊^承
純?cè)玩溊^承的思想就是讓所有需要繼承的屬性都在原型鏈上(而不會(huì)在實(shí)例對(duì)象自己身上)。
原型鏈?zhǔn)菍?shí)現(xiàn)繼承的重要工具家肯,以下是單純使用原型鏈實(shí)現(xiàn)繼承的方式:
function SuperType() {
this.property = 1;
}
// 將方法放在原型上龄砰,以便所有實(shí)例共用,避免重復(fù)定義
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = 2;
}
SubType.prototype = new SuperType();
// 將方法放在原型上讨衣,以便所有實(shí)例共用换棚,避免重復(fù)定義
SubType.prototype.getSubValue = function() {
return this.subproperty;
};
var instance = new SubType();
console.log(instance.getSuperValue()); // 1
console.log(instance instanceof SubType); // true
console.log(instance instanceof SuperType); // true
雖然上面使用了構(gòu)造函數(shù)與new關(guān)鍵字,但是在純?cè)玩溊^承中反镇,繼承屬性的原理與它們一點(diǎn)關(guān)系也沒(méi)有固蚤,父類的所有屬性都在原型鏈上。使用構(gòu)造函數(shù)只是為了代碼簡(jiǎn)潔歹茶,同時(shí)還可以讓我們使用instanceof
進(jìn)行對(duì)象識(shí)別(詳見(jiàn)上一篇文章)夕玩。
實(shí)際上,我們可以不使用構(gòu)造函數(shù)與new關(guān)鍵字來(lái)實(shí)現(xiàn)純?cè)玩溊^承:
// 定義父對(duì)象的原型
var superPrototype = {
getSuperValue: function() {
return this.property;
}
};
// 定義父對(duì)象的工廠方法
function createSuper() {
var superInstance = Object.create(superPrototype);
superInstance.property = 1;
return superInstance;
}
// 定義子對(duì)象的原型
var subPrototype = createSuper();
subPrototype.getSubValue = function() {
return this.subproperty;
};
// 定義子對(duì)象的工廠方法
function createSub() {
var subInstance = Object.create(subPrototype);
subInstance.subproperty = 2;
return subInstance;
}
// 創(chuàng)建子對(duì)象
var instance = createSub();
console.log(instance.getSuperValue()); // 1
console.log(instance.getSubValue()); // 2
通過(guò)這種方式實(shí)現(xiàn)純?cè)玩溊^承的作用完全相同惊豺,原型鏈也完全相同燎孟,只不過(guò)不能使用instanceof和constructor指針了(因?yàn)樗鼈冃枰獦?gòu)造函數(shù))。
所有需要繼承的屬性都在原型鏈上(而不會(huì)在實(shí)例對(duì)象自己身上)尸昧,這是純?cè)玩溊^承與后面繼承方式的最大不同揩页。
純?cè)玩溊^承的缺陷
- 在使用構(gòu)造函數(shù)的實(shí)現(xiàn)方式中,我們無(wú)法給父類的構(gòu)造函數(shù)傳遞參數(shù)烹俗。也就是上例中的這條語(yǔ)句:
SubType.prototype = new SuperType();
爆侣,這句話是在創(chuàng)建子對(duì)象之前就執(zhí)行的,所以我們無(wú)法在實(shí)例化的時(shí)候決定父類構(gòu)造函數(shù)的參數(shù)幢妄。 - 如果不選擇構(gòu)造函數(shù)的那種實(shí)現(xiàn)方式兔仰,會(huì)出現(xiàn)與創(chuàng)建對(duì)象模式相同的對(duì)象識(shí)別問(wèn)題。
- 原型鏈的改變會(huì)影響所有已經(jīng)構(gòu)造出的實(shí)例蕉鸳。這是一種靈活性乎赴,也是一種危險(xiǎn)。如果我們不小心通過(guò)實(shí)例對(duì)象改變了原型鏈上的屬性潮尝,會(huì)影響所有的實(shí)例對(duì)象无虚。
- 父類可能有一些屬性不適合共享,但是純?cè)玩溊^承強(qiáng)迫所有父類屬性都要共享衍锚。比如People是Student的父類,People的name屬性顯然是每個(gè)子對(duì)象都應(yīng)該獨(dú)享的嗤堰。如果使用純?cè)玩溊^承戴质,我們必須在每次得到子類對(duì)象以后手動(dòng)給子對(duì)象添加name屬性度宦。
2. 純構(gòu)造函數(shù)繼承
這種技術(shù)通過(guò)在子類構(gòu)造函數(shù)中調(diào)用父類的構(gòu)造函數(shù),使所有需要繼承的屬性都定義在實(shí)例對(duì)象上告匠。
function SuperType(fatherName) {
this.fatherName = fatherName;
this.fatherArray = ["red", "blue", "green"];
}
function SubType(childName, fatherName) {
SuperType.call(this, fatherName);
this.childName = childName;
}
var child1 = new SubType('childName1', 'fatherName1');
var child2 = new SubType('childName2', 'fatherName2');
child1.fatherArray.push("black");
console.log(child1.fatherArray); //"red,blue,green,black"
console.log(child2.fatherArray); //"red,blue,green"
由上例可知戈抄,純構(gòu)造函數(shù)繼承不會(huì)出現(xiàn)純?cè)玩溊^承的問(wèn)題:
- 不存在“原型鏈的改變影響所有已經(jīng)構(gòu)造出的實(shí)例”的問(wèn)題。這是因?yàn)椴还苁亲宇惖膶傩赃€是父類的屬性后专,在子對(duì)象上都有一個(gè)副本划鸽,因此改變一個(gè)對(duì)象的屬性不會(huì)影響另一個(gè)對(duì)象。
- 可以在構(gòu)造子對(duì)象實(shí)例的時(shí)候給父類構(gòu)造函數(shù)傳遞參數(shù)戚哎。在純構(gòu)造函數(shù)繼承中裸诽,父類構(gòu)造函數(shù)是在子類構(gòu)造函數(shù)中調(diào)用的,每次調(diào)用時(shí)型凳,傳遞給父構(gòu)造函數(shù)的參數(shù)可以通過(guò)子構(gòu)造函數(shù)的參數(shù)控制丈冬。
純構(gòu)造函數(shù)繼承的缺陷
- 與創(chuàng)建對(duì)象的構(gòu)造函數(shù)模式相同,低效率甘畅,函數(shù)重復(fù)定義埂蕊,無(wú)法復(fù)用。
- 如果子類要使用這種繼承方式疏唾,父類必須也要使用這種繼承方式蓄氧。因?yàn)?strong>使用這種方式的前提就是所有需要繼承的屬性都在父構(gòu)造函數(shù)上。如果父類有一些屬性是通過(guò)原型鏈繼承來(lái)的槐脏,那么子類僅僅通過(guò)調(diào)用父構(gòu)造函數(shù)無(wú)法得到這些屬性喉童。
- 對(duì)象識(shí)別功能不全。無(wú)法使用 instanceof 來(lái)判斷某個(gè)對(duì)象是否繼承自某個(gè)父類准给。但是還是有基本的對(duì)象識(shí)別功能:可以使用 instanceof 來(lái)判斷某個(gè)對(duì)象是不是某個(gè)類的實(shí)例泄朴。
3. 組合繼承
組合繼承就是同時(shí)使用原型鏈和構(gòu)造函數(shù)繼承:
- 對(duì)于那些適合共享的屬性(一般是函數(shù)),將它們放在原型鏈上露氮。
- 對(duì)于需要每個(gè)子對(duì)象獨(dú)享的屬性祖灰,在構(gòu)造函數(shù)中定義。
// 定義父類
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
// 定義子類
function SubType(name, age) {
// 這里調(diào)用了父對(duì)象的構(gòu)造函數(shù)(構(gòu)造函數(shù)繼承)
SuperType.call(this, name);
this.age = age;
}
// 這里創(chuàng)建了一個(gè)父對(duì)象來(lái)當(dāng)作原型(原型鏈繼承)
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
};
var instance1 = new SubType("Nicholas", 29);
var instance2 = new SubType("Greg", 27);
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
console.log(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
使用原型鏈的時(shí)候畔规,我們要作出合理的選擇:
- 哪些屬性是每個(gè)實(shí)例對(duì)象獨(dú)享的局扶,需要在每次實(shí)例化的時(shí)候添加到實(shí)例對(duì)象上。
- 哪些屬性是所有實(shí)例共享的叁扫,只需要定義在原型對(duì)象上三妈。這可以減少資源的浪費(fèi)。
組合繼承的代碼與純?cè)玩溊^承的代碼看起來(lái)很相似莫绣,它們的核心區(qū)別在于:組合繼承要在子類構(gòu)造函數(shù)中調(diào)用父類構(gòu)造函數(shù)畴蒲。
組合繼承避免了兩者的缺陷,結(jié)合了兩者的優(yōu)點(diǎn)对室,并且可以使用instanceof進(jìn)行對(duì)象識(shí)別模燥,因此組合繼承在JavaScript中經(jīng)常被使用咖祭。
組合繼承的缺陷
- 得到一個(gè)子對(duì)象要執(zhí)行兩次父構(gòu)造函數(shù),重復(fù)定義屬性蔫骂。在組合繼承中么翰,我們要執(zhí)行兩次構(gòu)造函數(shù):
- 在定義原型鏈的時(shí)候創(chuàng)建原型對(duì)象
- 在子類的構(gòu)造函數(shù)中調(diào)用父構(gòu)造函數(shù)來(lái)初始化新對(duì)象的屬性。
調(diào)用兩次構(gòu)造函數(shù)的結(jié)果就是重復(fù)定義了父類的屬性辽旋,第一次定義在原型鏈上浩嫌,第二次定義在子對(duì)象上。
如果你在Chrome控制臺(tái)中打印上面例子的instance2补胚,你會(huì)發(fā)現(xiàn)码耐,在原型對(duì)象和子對(duì)象上都有"name"和"colors"屬性。
這樣的重復(fù)定義顯然不夠優(yōu)雅糖儡,后面的寄生組合式繼承解決了這個(gè)問(wèn)題伐坏。
4. 原型式繼承
原型式繼承的核心是以父對(duì)象為原型直接創(chuàng)建子對(duì)象。
原型式繼承(Prototypal Inheritance)是由Douglas Crockford在他的一篇文章Prototypal Inheritance in JavaScript中提出的握联。js是一種基于原型的語(yǔ)言桦沉,然而Douglas卻發(fā)現(xiàn),當(dāng)時(shí)沒(méi)有操作符能夠方便地以一個(gè)對(duì)象為原型金闽,創(chuàng)建一個(gè)新對(duì)象纯露。js的原型的天性被構(gòu)造函數(shù)給掩蓋了,因?yàn)闃?gòu)造函數(shù)被很多人當(dāng)作“類”來(lái)使用代芜。因此Douglas提出一個(gè)簡(jiǎn)單的函數(shù)埠褪,以父對(duì)象為原型直接創(chuàng)建子對(duì)象,沒(méi)有類挤庇、沒(méi)有構(gòu)造函數(shù)钞速、沒(méi)有new操作符,只有對(duì)象繼承對(duì)象嫡秕,回歸js的本質(zhì):
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
在原型式繼承被提出的時(shí)候渴语,Object.create方法還沒(méi)有引入。ES5引入的Object.create()方法實(shí)際就是object函數(shù)的規(guī)范化昆咽。
有了object函數(shù)以后驾凶,為了能夠方便地創(chuàng)建對(duì)象,你可能需要使用工廠模式掷酗,來(lái)為得到的子對(duì)象增加屬性:
// 父對(duì)象
var superObject = {
superValue:1,
showSuperValue: function() {
console.log(this.superValue);
}
};
// 子對(duì)象的工廠方法
function createSub() {
var instance = Object.create(superObject);
instance.subValue = 2;
return instance;
}
var subObject = createSub();
subObject.showSuperValue(); // 1
我們有時(shí)不一定要大量制造子對(duì)象调违,此時(shí)就沒(méi)必要定義一個(gè)工廠函數(shù)了,直接使用Object.create()就好泻轰。原型式繼承不一定要定義一個(gè)工廠函數(shù)技肩,只要以父對(duì)象為原型直接創(chuàng)建子對(duì)象,就是一種原型式繼承浮声。
原型式繼承是一種純?cè)玩溊^承亩鬼,因?yàn)?strong>原型式繼承也將所有要繼承的屬性放在了原型鏈上殖告,我們?cè)诩冊(cè)玩溊^承中的第二種實(shí)現(xiàn)方式,實(shí)際上也是原型式繼承雳锋。
如果要給子對(duì)象添加函數(shù),不要在工廠函數(shù)中定義它羡洁,而要將函數(shù)放在原型對(duì)象上玷过,避免低效率代碼。我們?cè)诩冊(cè)玩溊^承中的第二種實(shí)現(xiàn)方式就是一個(gè)值得學(xué)習(xí)的例子筑煮。
5. 寄生式繼承
寄生式繼承的思想是增強(qiáng)父對(duì)象辛蚊,得到子對(duì)象。新特性“寄生”在父對(duì)象上真仲。
寄生式繼承使用工廠函數(shù)封裝了以下步驟:
- 使用父對(duì)象的創(chuàng)建方法(工廠函數(shù)或構(gòu)造函數(shù))袋马,創(chuàng)建父對(duì)象
- 增強(qiáng)父對(duì)象(給它增加屬性)
- 返回這個(gè)對(duì)象,它就是子對(duì)象
function Super() {
this.superProperty = 1;
}
Super.prototype.sayHi = function() {
console.log('hi');
};
function subFactory() {
var instance = new Super();
instance.subProperty = 2;
instance.sayGoodBye = function() {
console.log('goodbye');
};
return instance;
}
var sub = subFactory();
繼承得到的屬性可能在子對(duì)象上秸应,也可能在子對(duì)象的原型上虑凛,這取決于父對(duì)象的屬性在不在原型鏈上。
寄生式繼承的缺陷
- 如果在工廠函數(shù)中為對(duì)象添加函數(shù)(如上面的例子)软啼,那么會(huì)出現(xiàn)與純構(gòu)造函數(shù)繼承一樣的低效率問(wèn)題桑谍,函數(shù)重復(fù)定義。
- 不能使用進(jìn)行對(duì)象識(shí)別祸挪,因?yàn)樽訉?duì)象并不是通過(guò)構(gòu)造函數(shù)創(chuàng)建的锣披。
6. 寄生組合式繼承
我們前面說(shuō)過(guò),雖然組合繼承很常用贿条,但是它也有自己的不足:調(diào)用兩次父構(gòu)造函數(shù)雹仿,重復(fù)定義父屬性。第一次是在定義原型鏈的時(shí)候創(chuàng)建原型對(duì)象整以,第二次是在子類的構(gòu)造函數(shù)中調(diào)用父構(gòu)造函數(shù)來(lái)初始化新對(duì)象的屬性胧辽。以下是我們給出過(guò)的組合式繼承:
// 定義父類
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
// 定義子類
function SubType(name, age) {
// 第二次調(diào)用父構(gòu)造函數(shù)
SuperType.call(this, name);
this.age = age;
}
// 第一次調(diào)用父構(gòu)造函數(shù)
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
};
var instance1 = new SubType("Nicholas", 29);
var instance2 = new SubType("Greg", 27);
// instance的本身和原型對(duì)象上都有“name”和“colors”屬性
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
console.log(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
我們顯然應(yīng)該舍棄其中的一次。
可以舍棄第二次調(diào)用嗎悄蕾?不行票顾,那樣就變成純?cè)玩溊^承了,你可以回去看看這種繼承的缺陷帆调。
既然如此奠骄,我們只能舍棄第一次調(diào)用了。第一次調(diào)用構(gòu)造函數(shù)只是為了獲得父對(duì)象原型鏈上的屬性番刊,父對(duì)象實(shí)例上的屬性交給第二次調(diào)用來(lái)添加了含鳞。實(shí)際上我們不需要?jiǎng)?chuàng)建一個(gè)父對(duì)象也可以獲得父對(duì)象的原型鏈:
也就是說(shuō),我們可以把
SubType.prototype = new SuperType();
改成
SubType.prototype = Object.create(SuperType.prototype);
創(chuàng)建上圖中的藍(lán)色空對(duì)象芹务,并將它作為子類的原型蝉绷。子對(duì)象一樣可以繼承到父對(duì)象原型鏈上的所有屬性鸭廷!
因此,寄生組合式繼承的代碼就是這樣:
// 定義父類
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
// 定義子類
function SubType(name, age) {
// 獲得父對(duì)象實(shí)例上的屬性
SuperType.call(this, name);
this.age = age;
}
// 獲得父對(duì)象原型鏈上的屬性
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
};
var instance1 = new SubType("Nicholas", 29);
var instance2 = new SubType("Greg", 27);
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
console.log(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
現(xiàn)在我們?cè)贑hrome控制臺(tái)查看instance2的原型熔吗,發(fā)現(xiàn)原型鏈上已經(jīng)沒(méi)有“name”和“colors”屬性了辆床。寄生組合式繼承十分完美!
為什么不直接
SubType.prototype = SuperType.prototype;
呢桅狠?因?yàn)槲覀兒竺孢€要在SubType.prototype上添加子對(duì)象的共享函數(shù)讼载,如果使用Object.create創(chuàng)建一個(gè)新對(duì)象,會(huì)改變父對(duì)象的原型鏈中跌!
寄生組合式繼承真的與“寄生式繼承”有關(guān)嗎咨堤?對(duì)于這一點(diǎn)我持懷疑態(tài)度。按照《JavaScript高級(jí)程序設(shè)計(jì)(第3版)》的說(shuō)法漩符,因?yàn)槔^承的過(guò)程可以封裝成以下函數(shù):
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype); // 創(chuàng)建對(duì)象
prototype.constructor = subType; // 增強(qiáng)對(duì)象
subType.prototype = prototype; // 指定對(duì)象
}
而作者認(rèn)為這個(gè)函數(shù)是一種“寄生式繼承”:prototype“寄生式繼承”自superType.prototype一喘。
我認(rèn)為這是一種原型式繼承。不能因?yàn)樵诠S函數(shù)中獲取對(duì)象嗜暴、增強(qiáng)對(duì)象凸克,就認(rèn)為它是寄生式繼承。原型式繼承一樣可以用工廠函數(shù)來(lái)封裝灼伤。原型式繼承與寄生式繼承的區(qū)別在于獲取對(duì)象的方式:
- 通過(guò)object函數(shù)或Object.create触徐,以父對(duì)象為原型直接創(chuàng)建一個(gè)空對(duì)象。這是原型式繼承狐赡。
- 通過(guò)父類的構(gòu)造函數(shù)或工廠函數(shù)獲得對(duì)象撞鹉。這是寄生式繼承。
在Douglas Crockford的文章Prototypal Inheritance in JavaScript中颖侄,在介紹原型式繼承的時(shí)候他說(shuō)到:
"For convenience, we can create functions which will call the object function for us, and provide other customizations such as augmenting the new objects with privileged functions. I sometimes call these maker functions. If we have a maker function that calls another maker function instead of calling the object function, then we have a parasitic inheritance pattern."
原型式繼承一樣可以使用工廠函數(shù)鸟雏。當(dāng)調(diào)用另一個(gè)工廠函數(shù)而不是object函數(shù)的時(shí)候,原型式繼承才變成寄生式繼承览祖。
名字叫什么并不重要孝鹊,能夠?qū)⑦@些模式中的編程思想融會(huì)貫通就夠了。將來(lái)我們不一定有機(jī)會(huì)寫(xiě)“形式完全規(guī)范”的某種模式展蒂,但是其中的思想肯定會(huì)在一些零零散散的地方用上又活。