寫(xiě)了一段時(shí)間的js后,發(fā)覺(jué)自己還停留在面向過(guò)程的編程方式中,而這種方式非常不利于團(tuán)隊(duì)協(xié)作開(kāi)發(fā),會(huì)帶來(lái)大量的變量污染,當(dāng)自己或者別人想重復(fù)使用已經(jīng)寫(xiě)好一段時(shí)間的代碼時(shí)會(huì)感到很混亂,并且也不利于他人去修改以這種方式寫(xiě)好的代碼庫(kù),簡(jiǎn)而言之,影響團(tuán)隊(duì)代碼的維護(hù)郊愧。 而解決這一問(wèn)題的方法,則是使用面向?qū)ο蟮木幊谭绞?在ES6之前,js中并沒(méi)有很直觀的繼承這種機(jī)制,而是根據(jù)實(shí)際需求通過(guò)js的原型來(lái)實(shí)現(xiàn)不同樣式的繼承,而當(dāng)我在初次接觸js那多樣的繼承方式時(shí)直接懵逼了,故寫(xiě)下此文按照一種需求發(fā)展的方式來(lái)捋清這幾種不同樣式的繼承。
一. 類(lèi)式繼承
//聲明父類(lèi)構(gòu)造函數(shù)——?jiǎng)游?function Animal(nickName, age){
//共有屬性(引用類(lèi)型)
this.action = {
run: "the animal is running",
eat: "the animal is eating"
};
//共有屬性(值類(lèi)型)
this.nickName = nickName || "defaultName";
this.age = age || 0;
}
Animal.prototype.getNickName = function(){
return this.nickName;
}
//聲明子類(lèi)構(gòu)造函數(shù)——貓
function Cat(){
this.voice = "meow";
}
Cat.prototype = new Animal();
Cat.prototype.say = function(){
return this.voice;
}
var myCat = new Cat("Tom", 1);
var yourCat = new Cat("Andy", 2);
console.log(myCat.nickName); //defaultName
console.log(yourCat.nickName); //defaultName
myCat.action.run = "my cat is runing";
console.log(myCat.action.run); //my cat is running
console.log(yourCat.action.run); //my cat is running
myCat.age = 5;
console.log(myCat.age); //5
console.log(yourCat.age); //0
console.log
從上面的例子可以看到,實(shí)現(xiàn)了基本的繼承,但是有兩個(gè)問(wèn)題:
- 無(wú)法向父類(lèi)傳值,因而myCat和yourCat的nickName都是輸出同樣的默認(rèn)值defaultName捻脖。
- 當(dāng)其中一個(gè)子類(lèi)實(shí)例myCat對(duì)父類(lèi)Animal的引用類(lèi)型進(jìn)行修改后,影響到了另一個(gè)子類(lèi)實(shí)例yourCat碾阁∩镣澹可見(jiàn)兩個(gè)子類(lèi)的實(shí)例都是共享同一處父類(lèi)的,因此如果需求是實(shí)例之間不能共享父類(lèi)值那么類(lèi)式繼承的方式也是不能使用的遍膜。
另外這里還有一個(gè)需要注意的地方,當(dāng)我們對(duì)myCat.age進(jìn)行賦值的時(shí)候并沒(méi)有影響到y(tǒng)ourCat,同樣類(lèi)似myCat.action = "new"也是一樣的結(jié)果,而當(dāng)對(duì)Animal中的引用進(jìn)行修改,例如myCat.action.run = "myCat is running", 則是可以影響到父類(lèi)的,從而導(dǎo)致yourCat.action.run也發(fā)生變化爵赵。這是因?yàn)閖s的機(jī)制造成,比如當(dāng)對(duì)myCat.action.run進(jìn)行修改時(shí),js會(huì)先查找當(dāng)前myCat是否包含action屬性,發(fā)現(xiàn)沒(méi)有,然后會(huì)去進(jìn)行原型鏈的查詢(xún),同理,在讀取值操作時(shí)在沒(méi)有找到的情況下也會(huì)去進(jìn)行原型鏈的查詢(xún),但是賦值操作如myCat.age=5, 這類(lèi)則不會(huì)進(jìn)行原型鏈的查詢(xún)而是直接對(duì)myCat實(shí)例附加一個(gè)age屬性,仔細(xì)想想這種機(jī)制在js執(zhí)行效率以及適用性方面都會(huì)表現(xiàn)的很好,如果確實(shí)需要修改父類(lèi)的age,那么可以通過(guò)myCat.__proto__.age = 5來(lái)進(jìn)行修改等恐。
上面這一大段廢話(huà),是我當(dāng)時(shí)突然疑惑的地方,js基本功看來(lái)還是有點(diǎn)捉急呀( ⊙ o ⊙ )洲劣!如果您發(fā)現(xiàn)我的這個(gè)理解不太對(duì)或者太過(guò)表象,將非常感謝您能指正一下O(∩_∩)O
二.構(gòu)造函數(shù)繼承
通過(guò)構(gòu)造函數(shù)繼承的方式可以有效的解決類(lèi)式繼承中無(wú)法向父類(lèi)傳值的問(wèn)題以及對(duì)父類(lèi)引用類(lèi)型的屬性進(jìn)行修改導(dǎo)致其他子類(lèi)實(shí)例受到改動(dòng)的問(wèn)題。
//聲明父類(lèi)構(gòu)造函數(shù)——?jiǎng)游?function Animal(nickName, age){
//共有屬性(引用類(lèi)型)
this.action = {
run: "the animal is running",
eat: "the animal is eating"
};
this.test = [1,2,3,4];
//共有屬性(值類(lèi)型)
this.nickName = nickName || "defaultName";
this.age = age || 0;
}
Animal.prototype.getNickName = function(){
return this.nickName;
}
//聲明子類(lèi)構(gòu)造函數(shù)——貓
function Cat(nickName, age){
Animal.apply(this, [nickName,age]);
this.voice = "meow";
}
var myCat = new Cat("Tom", 1);
var yourCat = new Cat("Andy", 2);
console.log(myCat.nickName); //Tom
console.log(yourCat.nickName); //Andy
console.log(myCat.getNickName()); //Error: myCat.getNickName is not a function
同樣從上面的例子我們也能看出構(gòu)造函數(shù)繼承這種方式的問(wèn)題:
- 沒(méi)法獲取到父類(lèi)prototype中的方法或者屬性,因此在使用getNickName方法的時(shí)候提示沒(méi)有該方法课蔬。
- 而且這種方式當(dāng)創(chuàng)建多個(gè)子類(lèi)Cat的實(shí)例時(shí),這些實(shí)例中關(guān)于父類(lèi)構(gòu)造函數(shù)的屬性與方法都是各自單獨(dú)的一份,因此當(dāng)我們把本該共用的getNickName方法以this.getNickName的方式寫(xiě)入Animal構(gòu)造函數(shù)中,那么子類(lèi)Cat的多個(gè)實(shí)例都是各自擁有一個(gè)getNickName方法,因此沒(méi)有達(dá)到代碼復(fù)用的目的囱稽。
到這里,我們發(fā)現(xiàn)解決了一個(gè)問(wèn)題但又冒出來(lái)了兩個(gè)問(wèn)題,更何況在類(lèi)式繼承中還有一個(gè)問(wèn)題沒(méi)有解決。
三.組合繼承
為了解決在構(gòu)造函數(shù)繼承中無(wú)法獲取到父類(lèi)prototype的問(wèn)題,只需要調(diào)整一下子類(lèi)Cat的原型便可以了,即Cat.prototype = new Animal();
//聲明父類(lèi)——?jiǎng)游?function Animal(nickName, age){
//共有屬性(引用類(lèi)型)
this.action = {
run: "the animal is running",
eat: "the animal is eating"
};
this.test = [1,2,3,4];
//共有屬性(值類(lèi)型)
this.nickName = nickName || "defaultName";
this.age = age || 0;
}
Animal.prototype.getNickName = function(){
return this.nickName;
}
function Cat(nickName, age){
Animal.apply(this, [nickName,age]);
this.voice = "meow";
}
Cat.prototype = new Animal();
Cat.prototype.say = function(){
return this.voice;
}
var myCat = new Cat("Tom", 1);
var yourCat = new Cat("Andy", 2);
console.log(myCat.nickName); //Tom
console.log(yourCat.nickName); //Andy
console.log(myCat.getNickName()); //Tom
myCat.action.run = "myCat is running";
console.log(myCat.action.run); //myCat is running
console.log(yourCat.action.run); //the animal is running
如上例子所示,這種繼承方式,解決了前面我們遇到的以下幾個(gè)問(wèn)題:
- 無(wú)法向父類(lèi)傳值二跋。
- 各個(gè)實(shí)例之間共享父類(lèi)战惊。
- 可以獲取到父類(lèi)的原型上的屬性和方法。
因?yàn)槭墙M合了類(lèi)式繼承以及構(gòu)造函數(shù)繼承兩個(gè)方式的繼承方式,所以同樣有構(gòu)造函數(shù)繼承方式中的第二個(gè)問(wèn)題,即違背了代碼的復(fù)用扎即。其次,在子類(lèi)Cat.prototype的時(shí)候執(zhí)行了一次父類(lèi)Animal構(gòu)造函數(shù)吞获,在實(shí)例化子類(lèi)中又執(zhí)行了一次父類(lèi)Animal構(gòu)造函數(shù)后,可見(jiàn)這是有一定開(kāi)銷(xiāo)的。所以雖然能解決功能使用的問(wèn)題,但還不太令人滿(mǎn)意谚鄙。
四.原型式繼承
原型式繼承方式是道格拉斯提出來(lái)的一種實(shí)現(xiàn)繼承的方法各拷,當(dāng)時(shí)提出這種方式為了解決什么樣的問(wèn)題,我不太了解闷营,但根據(jù)上面在組合繼承方式面臨的開(kāi)銷(xiāo)問(wèn)題烤黍,原型式繼承中由于下面代碼中object方法中的臨時(shí)函數(shù)F是一個(gè)空函數(shù),所以開(kāi)銷(xiāo)相對(duì)不大傻盟,先來(lái)看看道格拉斯他的這段功能性的代碼:
function object(obj){
function F(){}
F.prototype = obj;
return new F();
}
要使用這個(gè)方法需要一個(gè)基對(duì)象作為參數(shù)傳入速蕊。在該方法內(nèi)部,有個(gè)臨時(shí)的構(gòu)造函數(shù)F娘赴,然后把傳入的基對(duì)象作為該臨時(shí)構(gòu)造函數(shù)的原型规哲,最后返回該臨時(shí)函數(shù)的實(shí)例。
而在ES5中新增的Object.create()方法更為強(qiáng)大诽表,可以作為該方法的替代品媳叨,而Object.create()方法的第二個(gè)參數(shù)對(duì)象上的任何屬性都會(huì)作為新生成實(shí)例的屬性:
var Animal = {
nickName: "defaultName",
actions: ["run", "eat", "drink"]
}
function object(obj){
function F(){}
F.prototype = obj;
return new F();
}
var animalA = object(Animal);
var animalB = Object.create(Animal);
animalA.actions.push("sleep");
console.log(animalA.nickName); //defaultName
console.log(animalA.actions); //["run", "eat", "drink", "sleep"]
console.log(animalB.actions); //["run", "eat", "drink", "sleep"]
var animalC = Object.create(Animal, {
nickName:{
value: "Jack"
},
actions: {
value: ["walk"]
}
});
console.log(animalC.nickName); //Jack
console.log(animalC.actions); //["walk"]
console.log(animalA.actions); //["run", "eat", "drink", "sleep"]
console.log(animalB.actions); //["run", "eat", "drink", "sleep"]
五.寄生式繼承
寄生式繼承其實(shí)就是對(duì)原型式繼承再次封裝,在這次封裝方法中可以進(jìn)行屬性的添加关顷,其實(shí)現(xiàn)效果和使用了Object.create()方法第二個(gè)參數(shù)后的效果是一致的糊秆,也就是也就是說(shuō)這種繼承方式完全可以被Object.create()方法代替,但是這種繼承方式的思想確會(huì)被應(yīng)用于等下會(huì)介紹的寄生組合式繼承方式中议双,而寄生組合式繼承將是我們最終比較完善的js繼承方式痘番。
var Animal = {
nickName: "defaultName",
actions: ["run", "eat", "drink"]
}
function object(obj){
function F(){}
F.prototype = obj;
return new F();
}
function createCat(obj){
var o = object(obj);
o.getName = function(){
return this.nickName;
}
return o;
}
var myCat = createCat(Animal);
console.log(myCat.getName()); //defaultName
六.寄生組合式繼承
先來(lái)看寄生組合式繼承的核心代碼,也就是根據(jù)寄生式繼承的思想來(lái)實(shí)現(xiàn)的:
function inheritPrototype(subType, superType){
var o = object(superType.prototype);
o.constructor = subType;
subType.prototype = o;
}
會(huì)傳入兩個(gè)參數(shù),分別是子類(lèi)構(gòu)造函數(shù)subType汞舱,以及父類(lèi)構(gòu)造函數(shù)superType伍纫,然后通過(guò)原型式繼承方式得到一個(gè)把父類(lèi)構(gòu)造函數(shù)的原型作為其原型的對(duì)象,即上述代碼中的變量o昂芜,然后只需要讓子類(lèi)的原型指向這個(gè)o即可莹规,如此便能解決組合繼承方式中不能有效的復(fù)用共用函數(shù)的問(wèn)題。但是若直接賦值o給子類(lèi)原型后還會(huì)有一個(gè)問(wèn)題泌神,就是subType丟失了默認(rèn)的constructor屬性良漱,于是subType在prototype中查詢(xún)constructor時(shí)會(huì)找不到,從而進(jìn)行原型鏈查找欢际,于是就把superType的constructor屬性作為subType的構(gòu)造函數(shù)了母市,于是這改變了子類(lèi)的構(gòu)造函數(shù),但是對(duì)于我們的繼承功能的使用來(lái)講這并沒(méi)有什么大的影響损趋,但是假如當(dāng)我們寫(xiě)插件提供給別人的都是實(shí)例化后的對(duì)象比如myCat時(shí)患久,如果別人想擴(kuò)展下我們的Cat對(duì)象,就可以通過(guò)myCat.constructor.prototype去修改或擴(kuò)展我們的Cat類(lèi)了浑槽。因此在把o賦值給subType的原型之前我們需要o.constructor = subType蒋失,添加一下constructor的指向?yàn)樽宇?lèi)subType。
function object(obj){
function F(){}
F.prototype = obj;
return new F();
}
function inheritPrototype(subType, superType){
var o = object(superType.prototype);
o.constructor = subType;
subType.prototype = o;
}
//聲明父類(lèi)——?jiǎng)游?function Animal(nickName, age){
//共有屬性(引用類(lèi)型)
this.action = {
run: "the animal is running",
eat: "the animal is eating"
};
//共有屬性(值類(lèi)型)
this.nickName = nickName || "defaultName";
this.age = age || 0;
}
Animal.prototype.getNickName = function(){
return this.nickName;
}
function Cat(nickName, age){
Animal.apply(this, [nickName,age]);
this.voice = "meow";
this.niubi = 0;
}
inheritPrototype(Cat, Animal);
Cat.prototype.say = function(){
return this.voice;
}
var myCat = new Cat("Tom", 1);
var yourCat = new Cat("Andy", 2);
myCat.action.run = "myCat is running";
console.log(myCat.action.run); // myCat is running
console.log(yourCat.action.run); // the animal is running
可見(jiàn)通過(guò)inheritPrototype方法我們實(shí)現(xiàn)了子類(lèi)對(duì)父類(lèi)的原型的繼承桐玻,從而解決了多個(gè)實(shí)例無(wú)法共享使用同一段代碼的代碼不能復(fù)用的問(wèn)題高镐,并且使用inheritPrototye方法代替了組合式繼承中的Cat.prototype = new Animal()來(lái)避免了多次調(diào)用父類(lèi)構(gòu)造函數(shù)的開(kāi)銷(xiāo)。顯然畸冲,在子類(lèi)Cat中使用了構(gòu)造函數(shù)繼承的思想嫉髓。在利用了寄生式繼承思想的inheritPrototye方法。配合上構(gòu)造函數(shù)繼承方式邑闲,也就形成了這個(gè)比較完善的寄生組合式繼承算行。
當(dāng)然js在面向?qū)ο蟮拈_(kāi)發(fā)方式中,遠(yuǎn)不止這一點(diǎn)東西苫耸,這些都是一些很基礎(chǔ)知識(shí)點(diǎn)州邢,再此梳理一下,文中有些表達(dá)不準(zhǔn)確或者理解的不到位或者不對(duì)的地方褪子,還希望大家能指正一下O(∩_∩)O謝謝