創建對象的模式多種多樣,但是各種模式又有怎樣的利弊呢?有沒有一種最為完美的模式呢?下面我將就以下幾個方面來分析創建對象的幾種模式:
我之前在博文《JavaScript中對象字面量的理解 http://www.linuxidc.com/Linux/2016-11/136666.htm》中講到過這兩種方法,如何大家不熟悉,可以點進去看一看回顧一下。它們的優點是用來創建單個的對象非常方便。但是這種方法有一個明顯的缺點:利用同一接口創建很多對象是,會產生大量的重復代碼。這句話怎麼理解呢?讓我們看一下下面的代碼:
1 2 3 4var
person1={
<strong>name</strong>:
"zzw"
,
<strong>age</strong>:
"21"
,
<strong>school</strong>:
"xjtu"
,<br> <strong> sayName</strong>:<strong>
function
(){</strong><br><strong> console.log(
this
.name);</strong><br><strong> };</strong>
1
2
3
4
5
}
var
person2={
<strong>name</strong>:
"ht"
,
<strong>age</strong>:
"18"
,
<strong>school</strong>:
"tjut"
,<br> <strong> sayName:
function
(){</strong><br><strong> console.log(
this
.name);</strong><br><strong> };</strong><br><br> }
可以看出,當我們創建了兩個類似的對象時,我們重復寫了name age school 以及對象的方法這些代碼,隨著類似對象的增多,顯然,代碼會凸顯出復雜、重復的感覺。為解決這一問題,工廠模式應運而生。
剛剛我們提到:為解決創建多個對象產生大量重復代碼的問題,由此產生了工廠模式。那麼,究竟什麼是工廠模式?它是如何解決這一問題的呢?首先,我們可以想一想何謂工廠? 就我個人理解:在工廠可以生產出一個模具,通過這個模具大量生產產品,最終我們可以加以修飾(比如噴塗以不同顏色,包裝不同的外殼)。這樣就不用一個一個地做產品,由此可以大大地提高效率。
同樣地,對於創建對象也是這樣的思路:它會通過一個函數封裝創建對象的細節。最後直接將不同的參數傳遞到這個函數中去,以解決產生大量重復代碼的問題。觀察以下代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13
function
createPerson(name,age,school){
var
o=
new
Object();
o.name=name;
o.age=age;
o.school=school;
o.sayName=
function
(){
console.log(
this
.name);
};
return
o;
}
var
person1=createPerson(
"zzw"
,
"21"
,
"xjtu"
);
var
person2=createPerson(
"ht"
,
"18"
,
"tjut"
);
看似這裡的代碼也不少啊!可是,如果在多創建2個對象呢,10個呢,100個呢?結果可想而知,於是工廠模式成功地解決了Object構造函數或對象字面量創建單個對象而造成大量代碼重復的問題!工廠模式有以下特點:
但是,我們仔細觀察,可以發現工廠模式創建的對象,例如這裡創建的person1和person2,我們無法直接識別對象是什麼類型。為了解決這個問題,自定義的構造函數模式出現了。
剛剛說到,自定義構造函數模式是為了解決無法直接識別對象的類型才出現的。那麼顯然自定義構造函數模式至少需要解決兩個問題。其一:可以直接識別創建的對象的類型。其二:解決工廠模式解決的創建大量相似對象時產生的代碼重復的問題。
那麼,我為什麼說是自定義構造函數模式呢?這是因為,第一部分中,我們使用的Object構造函數是原生構造函數���顯然它是解決不了問題的。只有通過創建自定義的構造函數,從而定義自定義對象類型的屬性和方法。代碼如下:
1 2 3 4 5 6 7 8 9 10function
Person(name,age,school){
this
.name=name;
this
.age=age;
this
.school=school;
this
.sayName=
function
(){
console.log(
this
.name);
};
}
var
person1=
new
Person(
"zzw"
,
"21"
,
"xjtu"
);
var
person2=
new
Person(
"ht"
,
"18"
,
"tjut"
);
首先我們驗證這種自定義的構造模式是否解決了第一個問題。在上述代碼之後追加下面的代碼:
1 2console.log(person1 <strong>
instanceof
</strong> Person);
//true
console.log(person1 <strong>
instanceof
</strong> Object);
//true
結構都得到了true,對於Object當然沒有問題,因為一切對象都是繼承自Object的,而對於Person,我們在創建對象的時候用的是Person構造函數,那麼得到person1是Person類型的也就沒問題了。
對於第二個問題,答案是顯而易見的。很明顯,創建大量的對象不會造成代碼的重復。於是,自定義構造函數成功解決所有問題。
A 下面我們對比以下自定義構造函數與工廠模式的不同之處:
B 對於構造函數,我們還應當注意:
C 如何理解構造函數也是函數?
只要證明構造函數也可以像普通函數一樣的調用,那麼就可以理解構造函數也是函數了。
1 2 3 4 5 6 7 8 9 10function
Person(name,age,school){
this
.name=name;
this
.age=age;
this
.school=school;
this
.sayName=
function
(){
console.log(
this
.name);
};
}
<strong>Person(
"zzw"
,
"21"
,
"xjtu"
);</strong>
sayName();
//zzw
可以看出,我直接使用了Person("zzw","21","xjtu");來像普通函數一樣的調用這個構造函數,因為我們把它當作了普通函數,那麼函數中的this就不會指向之前所說的對象(這裡亦沒有對象),而是指向了window。於是,函數一經調用,內部的變量便會放到全局環境中去,同樣,對於其中的函數也會在調用之後到全局環境,只是這個內部的函數是函數表達式並未被調用。只有調用即sayName();才能正確輸出。
由此,我們證明了構造函數也是函數。
D 那麼這種自定義構造函數就沒有任何問題嗎?
構造函數的問題是在每次創建一個實例時,構造函數的方法都需要再實例上創建一遍。由於在JavaScript中,我們認為所有的函數(方法)都是對象,所以每當創建一個實例對象,都會同時在對象的內部創建一個新的對象(這部分內容同樣可以在我的博文《JavaScript函數之美~》中找到)。即我們之前創建的自定義構造函數模式相當於下列代碼:
1 2 3 4 5 6
function
Person(name,age,school){
this
.name=name;
this
.age=age;
this
.school=school;
this
.sayName=
new
Function(
"console.log(this.name)"
);
}
?
1
2
var
person1=
new
Person(
"zzw"
,
"21"
,
"xjtu"
);
var
person2=
new
Person(
"ht"
,
"18"
,
"tjut"
);
即我們在創建person1和person2的時候,同時創建了兩個sayName為對象指針的對象,我們可以通過下面這個語句做出判斷:
1console.log(person1.sayName==person2.sayName);
//false
這就證明了如果創建兩個對象同時也在每個對象中又各自創建了一個函數對象,但是創建兩個完成同樣任務的Function實例的確沒有必要(況且內部有this對象,只要創建一個對象,this便會指向它)。這就造成了內部方法的重復造成資源浪費。
E 解決方法。
如果我們將構造函數內部的方法放到構造函數的外部,那麼這個方法便會被person1和person2共享了,於是,在每次創建新對象時就不會同時創建這個方法對象了。如下:
1 2 3 4 5 6 7 8 9 10 11
function
Person(name,age,school){
this
.name=name;
this
.age=age;
this
.school=school;
this
.sayName=sayName;
}
<strong>
function
sayName(){
console.log(
this
.name);
}</strong>
var
person1=
new
Person(
"zzw"
,
"21"
,
"xjtu"
);
var
person2=
new
Person(
"ht"
,
"18"
,
"tjut"
);
person1.sayName();//zzw
應當注意:this.sayName=sayName;中這裡等式右邊的sayName是一個指針,所以在創建新對象的時候只是創建了一個指向共同對像那個的指針而已,並不會創建一個方法對象。這樣便解決了問題。 而外面的sayName函數在最後一句中是被對象調用的,所以其中的this同樣是指向了對象。
F新的問題
如果這個構造函數中需要的方法很多,那麼為了保證能夠解決E中的問題,我們需要把所有的方法都寫在構造函數之外,可是如果這樣:
由此,為了解決F中的問題,接下來不得不提到JavaScript語言中的核心原型模式了。
為什麼會出現原型模式呢?這個模式在上面講了是為了解決自定義構造函數需要將方法放在構造函數之外造成封裝性較差的問題。當然它又要解決構造函數能夠解決的問題,所以,最終它需要解決以下幾個問題。其一:可以直接識別創建的對象的類型。其二:解決工廠模式解決的創建大量相似對象時產生的代碼重復的問題。其三:解決構造函數產生的封裝性不好的問題。由於這個問題比較復雜,所以我會分為幾點循序漸進的做出說明。
首先,我們應當知道:無論什麼時候,只要創建了一個新函數(函數即對象),就會根據一組特定的規則創建一個函數(對象)的prototype屬性(理解為指針),這個屬性會指向函數的原型對象(原型對象也是一個對象),但是因為我們不能通過這個新函數訪問prototype屬性,所以寫為[[prototype]]。同時,對於創建這個對象的構造函數也將獲得一個prototype屬性(理解為指針),同時指向它所創建的函數(對象)所指向的原型對象,這個構造函數是可以直接訪問prototype屬性的,所以我們可以通過訪問它將定義對象實例的信息直接添加到原型對象中。這時原型對象擁有一個constructor屬性(理解為指針)指向創建這個對象的構造函數(注意:這個constructor指針不會指向除了構造函數之外的函數)。
你可能會問?所有的函數都是由構造函數創建的嗎?答案是肯定的。函數即對象,我在博文《JavaScript函數之美~》中做了詳盡介紹。對與函數聲明和函數表達式這樣建立函數的方法本質上也是由構造函數創建的。
上面的說法可能過於抽象,我們先寫出一個例子(這個例子還不是我們最終想要的原型模式,只是為了讓大家先理解原型這個概念),再根據代碼作出說明:
1 2 3 4 5 6 7 8 9 10 11 12<strong>
function
Person(){}</strong>
Person.prototype.name=
"zzw"
;
Person.prototype.age=21;
Person.prototype.school=
"xjtu"
;
Person.prototype.sayName=
function
(){
console.log(
this
.name);
};
var
person1=
new
Person();
var
person2=
new
Person();
person1.sayName();
//zzw
person2.sayName();
//zzw
console.log(person1.sayName==person2.sayName);
//true
在這個例子中,我們首先創建了一個內容為空的構造函數,因為剛剛講了我們可以通過訪問構造函數的prototype屬性來為原型對象中添加屬性和方法。於是在下面幾行代碼中,我們便通過訪問構造函數的prototype屬性向原型對象中添加了屬性和方法。接著,創建了兩個對象實例person1和person2,並調用了原型對象中sayName()方法,得到了原型對象中的name值。這說明:構造函數創建的每一個對象和實例都擁有或者說是繼承了原型對象的屬性和方法。(因為無論是創建的對象實例還是創造函數的prototype屬性都是指向原型對象的) 換句話說,原型對象中的屬性和方法會被構造函數所創建的對象實例所共享,這也是原型對象的一個好處。
下面我會畫一張圖來繼續闡述這個問題:
從這張圖中我們可以看出以下幾點:
為了加深對原型的理解,我在這裡先介紹兩種方法確定構造函數創建的實例對象與原型對象之間的關系。
第一種方法:isPrototypeOf()方法,通過原型對象調用,確定原型對象是否是某個實例的原型對象。在之前的代碼後面追加下面兩句代碼:
1 2console.log(Person.prototype.isPrototypeOf(person1));
//true
console.log(Person.prototype.isPrototypeOf(person2));
//true
結果不出意外地均為true,也就是說person1實例和person2實例的原型對象都是Person.prototype。
第二種方法:Object.getPrototypeOf()方法,通過此方法得到某個對象實例的原型。在之前的代碼後面追加下面三句代碼:
1 2console.log(Object.getPrototypeOf(person1));
console.log(Object.getPrototypeOf(person1)==Person.prototype);<br> console.log(Object.getPrototypeOf(person1).name);
//zzw
其中第一句代碼在控制台中可以直接獲得person1的原型對象,如下圖所示:
其中第二句代碼得到布爾值:true。第三句代碼得到了原型對象中的name屬性值。
但是,當實例自己本身有和原型中相同的屬性名,而屬性值不同,在代碼獲取某個對象的屬性時,該從哪裡獲取呢?
規則是:在代碼讀取某個對象而某個屬性是,都會執行一次搜索,目標是具有給定名字的屬性。搜索首先從實例本身開始,如果在實例中找到了給定名字的屬性,則返回該屬��的值;如果沒有找到,則繼續搜索指針指向的原型對象。觀察下面的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13function
Person(){}
Person.prototype.name=
"zzw"
;
Person.prototype.age=21;
Person.prototype.school=
"xjtu"
;
Person.prototype.sayName=
function
(){
console.log(
this
.name);
};
var
person1=
new
Person();
var
person2=
new
Person();
console.log(person1.name);
//zzw
<strong>person1.name=
"htt"
;</strong>
<strong>console.log(person1.name);
//htt</strong>
console.log(person2.name);
//zzw<br> <strong> delete</strong> person1.name;<br> <strong> console.log(person1.name);//zzw</strong><br>
第三種方法:hasOwnProperty()方法
該方法可以檢測一個屬性是存在於實例中還是存在於原型中。只有給定屬性存在於對象實例中時,才會返回true,否則返回false。舉例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
function
Person(){}
Person.prototype.name=
"zzw"
;
Person.prototype.age=21;
Person.prototype.school=
"xjtu"
;
Person.prototype.sayName=
function
(){
console.log(
this
.name);
};
var
person1=
new
Person();
var
person2=
new
Person();
console.log(person1.name);
//zzw
<strong> console.log(person1.hasOwnProperty(
"name"
));
//false 因為zzw是搜索於原型對象的</strong>
person1.name=
"htt"
;
console.log(person1.name);
//htt
<strong> console.log(person1.hasOwnProperty(
"name"
));
//true 在上上一句,我添加了person1實例的屬性,它不是屬於原型對象的屬性</strong>
delete
person1.name;
console.log(person1.name);
//zzw
<strong> console.log(person1.hasOwnProperty(
"name"
));
//false 由於使用delete刪除了實例中的name屬性,所以為false
</strong>
in操作符會在通過對象能夠訪問給定屬性時,返回true,無論該屬性存在於事例中還是原型中。觀察下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
function
Person(){}
Person.prototype.name=
"zzw"
;
Person.prototype.age=21;
Person.prototype.school=
"xjtu"
;
Person.prototype.sayName=
function
(){
console.log(
this
.name);
};
var
person1=
new
Person();
var
person2=
new
Person();
console.log(person1.name);
//zzw
console.log(person1.hasOwnProperty(
"name"
));
//false
<strong>console.log(
"name"
in
person1);
//true</strong>
person1.name=
"htt"
;
console.log(person1.name);
//htt
console.log(person1.hasOwnProperty(
"name"
));
//true
<strong>console.log(
"name"
in
person1);
//true</strong>
delete
person1.name;
console.log(person1.name);
//zzw
console.log(person1.hasOwnProperty(
"name"
));
//false
<strong> console.log(
"name"
in
person1);
//true
</strong>
可以看到,確實,無論屬性在實例對象本身還是在實例對象的原型對象都會返回true。
有了in操作符以及hasOwnProperty()方法我們就可以判斷一個屬性是否存在於原型對象了(而不是存在於對象實例或者是根本就不存在)。編寫hasPrototypeProperty()函數並檢驗:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
function
Person(){}
<strong>
function
hasPrototypeProperty(Object,name){
return
!Object.hasOwnProperty(name)&&(name
in
Object);
}</strong>
Person.prototype.name=
"zzw"
;
Person.prototype.age=21;
Person.prototype.school=
"xjtu"
;
Person.prototype.sayName=
function
(){
console.log(
this
.name);
};
var
person1=
new
Person();
var
person2=
new
Person();
console.log(person1.name);
//zzw
<strong>console.log(hasPrototypeProperty(person1,
"name"
));
//true</strong>
person1.name=
"htt"
;
console.log(person1.name);
//htt
<strong> console.log(hasPrototypeProperty(person1,
"name"
));
//true</strong>
delete
person1.name;
console.log(person1.name);
//zzw
<strong> console.log(hasPrototypeProperty(person1,
"name"
));
//true
</strong>
其中hasPrototypeProperty()函數的判斷方式是:in操作符返回true而hasOwnProperty()方法返回false,那麼如果最終得到true則說明屬性一定存在於原型對象中。(注意:邏輯非運算符!的優先級要遠遠高於邏輯與&&運算符的優先級)
在通過for-in循環時,它返回的是所有能夠通過對象訪問的、可枚舉的屬性,其中既包括存在於實例中的屬性,也包括存在於原型中的屬性。且對於屏蔽了原型中不可枚舉的屬性(即將[[Enumerable]]標記為false的屬性)也會在for-in中循環中返回。(注:IE早期版本中存在一個bug,即屏蔽不可枚舉屬性的實例屬性不會出現在for-in循環中,這裡不做詳細介紹)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16function
Person(){}<br> Person.prototype.name=
"zzw"
;
Person.prototype.age=21;
Person.prototype.school=
"xjtu"
;
Person.prototype.sayName=
function
(){
console.log(
this
.name);
};
var
person1=
new
Person();
var
person2=
new
Person();
console.log(person1.name);
//zzw
person1.name=
"htt"
;
console.log(person1.name);
//htt
delete
person1.name;
console.log(person1.name);
//zzw
for
(
var
propName
in
person1){
console.log(propName);
//name age school sayName
}
通過for-in循環,我們可以枚舉初name age school sayName這幾個屬性。由於person1中的[[prototype]]屬性不可被訪問,因此,我們不能利用for-in循環枚舉出它。
Object.keys()方法接收一個參數,這個參數可以是原型對象,也可以是由構造函數創建的實例對象,返回一個包含所有可枚舉屬性的字符串數組。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
function
Person(){}
Person.prototype.name=
"zzw"
;
Person.prototype.age=21;
Person.prototype.school=
"xjtu"
;
Person.prototype.sayName=
function
(){
console.log(
this
.name);
};
var
person1=
new
Person();
var
person2=
new
Person();
console.log(person1.name);
//zzw
person1.name=
"htt"
;
console.log(person1.name);
//htt
person1.age=
"18"
;
<strong> console.log(Object.keys(Person.prototype));
//["name", "age", "school", "sayName"]
console.log(Object.keys(person1));
//["name", "age"]
console.log(Object.keys(person2));
//[]
</strong>
我們可以從上面的例子中看到,Object.keys()方法返回的是其自身的屬性。如原型對象只返回原型對象中的屬性,對象實例也只返回對象實例自己創建的屬性,而不返回繼承自原型對象的實例。
在之前的例子中,我們在構造函數的原型對象中添加屬性和方法時,每次都要在前面敲一遍Person.prototype,如果屬性多了,這樣的方法會顯得更為繁瑣,那麼下面我將介紹給大家一種簡單的方法。
我們知道,原型對象說到底它還是個對象,只要是個對象,我們就可以使用對象字面量方法來創建,方法如下:
1 2 3 4 5 6 7 8 9
function
Person(){}
Person.prototype={
name:
"zzw"
,
age:21,
school:
"xjtu"
,
sayName:
function
(){
console.log(
this
.name);
}
};
//原來利用Person.prototype.name="zzw"知識對象中的屬性,對於對象並沒有任何影響,而這裡創建了新的對象<br>
同樣,最開始,我們創建一個空的Person構造函數(大家發現了沒有,其實每次我們創建的都是空的構造函數),然後用對象字面量的方法來向原型對象中添加屬性。這樣既減少了不必要的輸入,也從視覺上更好地封裝了原型。 但是,這時原型對象的constructor就不會指向Person構造函數而是指向Object構造函數了。
為什麼會這樣?我們知道,當我們創建Person構造函數時,就會同時自動創建這個Person構造函數的原型(prototype)對象,這個原型對象也自動獲取了一個constructor屬性並指向Person構造函數,這個之前的圖示中可以清楚地看出來。之前我們使用的較為麻煩的方法(e.g. Person.prototype.name="zzw")只是簡單地向原型對象添加屬性,並沒有其他本質的改變。然而,上述這種封裝性較好的方法即使用對象字面量的方法,實際上是使用Object構造函數創建了一個新的原型對象(對象字面量本質即利用Object構造函數創建新對象),注意:此時Person構造函數的原型對象不再是之前的原型對象(而之前的原型對象的constructor屬性仍然指向Person構造函數),而和Object構造函數的原型對象一樣均為這個新的原型對象。這個原型對象和創建Person構造函數時自動生成的原型對象風馬牛不相及。理所應當的是,對象字面量創建的原型對象的constructor屬性此時指向了Object構造函數。
我們可以通過下面幾句代碼來驗證:
1 2 3 4 5 6 7 8 9 10 11 12
function
Person(){}
Person.prototype={
name:
"zzw"
,
age:21,
school:
"xjtu"
,
sayName:
function
(){
console.log(
this
.name);
}
};
var
person1=
new
Person();
console.log(Person.prototype.constructor==Person);
//false
console.log(Person.prototype.constructor==Object);
//true
通過最後兩行代碼我們可以看出Person構造函數的原型對象的constructor屬性此時不再指向Person構造函數,而是指向了Object構造函數。但是這並被影響我們正常使用,下面幾行代碼便可以清楚地看出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
function
Person(){}
Person.prototype={
name:
"zzw"
,
age:21,
school:
"xjtu"
,
sayName:
function
(){
console.log(
this
.name);
}
};
var
person1=
new
Person();
console.log(person1.name);
//zzw
console.log(person1.age);
//21
console.log(person1.school);
//xjtu
person1.sayName();
//zzw
下面我將以個人的理解用圖示表示(如果有問題,請指出):
第一步:創建一個空的構造函數。function Person(){}。此時構造函數的prototype屬性指向原型對象,而原型對象的constructor屬性指向Person構造函數。
第二步:利用對象字面量的方法創建一個Person構造函數的新原型對象。
1 2 3 4 5 6 7 8 Person.prototype={
name:
"zzw"
,
age:21,
school:
"xjtu"
,
sayName:
function
(){
console.log(
this
.name);
}
};
此時,由於創建了Person構造函數的一個新原型對象,所以Person構造函數的prototype屬性不再指向原來的原型對象,而是指向了Object構造函數創建的原型對象(這是對象字面量方法的本質)。但是原來的原型對象的constructor屬性仍指向Person構造函數。
第三步:由Person構造函數創建一個實例對象。
這個對象實例的constructor指針同構造它的構造函數一樣指向新的原型對象。
總結:從上面的這個例子可以看出,雖然新創建的實例對象仍可以共享添加在原型對象裡面的屬性,但是這個新的原型對象卻不再指向Person構造函數而指向Object構造函數,如果constructor的值真的非常重要的時候,我們可以像下面的代碼這樣重新設置會適當的值:
1 2 3 4 5 6 7 8 9 10function
Person(){}
Person.prototype={
constructor:Person,
name:
"zzw"
,
age:21,
school:
"xjtu"
,
sayName:
function
(){
console.log(
this
.name);
}
};
這樣,constructor指針就指回了Person構造函數。即如下圖所示:
值得注意的是:這種方式重設constructor屬性會導致它的[[Enumerable]]特性設置位true,而默認情況下,原生的constructor屬性是不可枚舉的。但是我們可以試用Object.defineProperty()將之修改為不可枚舉的(這一部分可以參見我的另一篇博文:《深入理解JavaScript中的屬性和特性》)。
原型的重要性不僅體現在自定義類型方面,就連所有原生的引用類型,都是使用這種模式創建的。所有原生引用類型(Object、Array、String,等等)都在其構造函數的原型上定義了方法。例如在Array.prototype中可以找到sort()方法,而在String.prototype中就可以找到substring()方法。
1 2console.log(
typeof
Array.prototype.sort);
//function
console.log(
typeof
String.prototype.substring);
//function
於是,實際上我們是可以通過原生對象的原型來修改它。比如:
1 2 3 4 5String.prototype.output=
function
(){
alert(
"This is a string"
);
}
var
message=
"zzw"
;
message.output();
這是,便在窗口中彈出了“This is a string”。盡管可以這樣做,但是我們不推薦在產品化的程序中修改原生對象的原型。這樣做有可能導致命名沖突等問題。
G.原型模式存在的問題
實際上,從上面對原型的講解來看,原型模式還是有很多問題的,它並沒有很好地解決我在第四部分初提出的若干問題:“其一:可以直接識別創建的對象的類型。其二:解決工廠模式解決的創建大量相似對象時產生的代碼重復的問題。其三:解決構造函數產生的封裝性不好的問題。”其中第一個問題解決的不錯,通過構造函數便可以直接看出來類型。第二個問題卻解決的不好,因為它省略了為構造函數傳遞初始化參數這一環節,結果所有的實例在默認情況下都將取得相同的默認值,我們只能通過在實例上添加同名屬性來屏蔽原型中的屬性,這無疑也會造成代碼重復的問題。第三個問題,封裝性也還說的過去。因此原型模式算是勉強解決了上述問題。
但是這種方法還由於本身產生了額外的問題。看下面的例子:
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
function
Person(){}
Person.prototype={
constructor:Person,
name:
"zzw"
,
age:21,
school:
"xjtu"
,
friends:[
"pengnian"
,
"zhangqi"
],
sayName:
function
(){
console.log(
this
.name);
}
};
var
person1=
new
Person();
var
person2=
new
Person();
person1.friends.push(
"feilong"
);
console.log(person1.friends);
//["pengnian","zhangqi","feilong"]
console.log(person2.friends);
//["pengnian","zhangqi","feilong"]
這裡我在新建的原型對象中增加了一個數組,於是這個數組會被後面創建的實例所共享,但是person1.friends.push("feilong");這句代碼我的意思是添加為person1的朋友而不是person2的朋友,但是在結果中我們可以看到person2的朋友也有了feilong,這就不是我們所希望的了。這也是對於包含引用類型的屬性的最大問題。
也正是這個問題和剛剛提到的第二個問題(即它省略了為構造函數傳遞初始化參數這一環節,結果所有的實例在默認情況下都將取得相同的默認值,我們只能通過在實例上添加同名屬性來屏蔽原型中的屬性,這無疑也會造成代碼重復的問題),很少有人會單單使用原型模式。
剛剛我們說到的原型模式存在的兩個最大的問題。問題一:由於沒有在為構造函數創建對象實例時傳遞初始化參數,所有的實例在默認情況下獲取了相同的默認值。問題二:對於原型對象中包含引用類型的屬性,在某一個實例中修改引用類型的值,會牽涉到其他的實例,這不是我們所希望的。而組合使用自定義構造函數模式和原型模式即使構造函數應用於定義實例屬性,而原型模式用於定義方法和共享的屬性。它能否解決問題呢?下面我們來一探究竟!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18function
Person(name,age,school){
this
.name=name;
this
.age=age;
this
.school=school;
this
.friends=[
"pengnian"
,
"zhangqi"
];
}
Person.prototype={
constructor:Person,
sayName:
function
(){
console.log(
this
.name);
}
}
var
person1=
new
Person(
"zzw"
,21,
"xjtu"
);
var
person2=
new
Person(
"ht"
,18,
"tjut"
);
person1.friends.push(
"feilong"
);
console.log(person1.friends);
//["pengnian", "zhangqi", "feilong"]
console.log(person2.friends);
//["pengnian", "zhangqi"]
console.log(person1.sayName==person2.sayName);
//true
OK!我們來看看組合使用構造函數模式和原型模式解決的問題:
綜上所述,組合使用構造函數模式和原型模式可以說是非常完美了。
實際上,組合使用構造函數模式和原型模式確實已經非常完美了,這裡將要講的幾種模式都是在特定的情況下使用的,所以我認為第六部分相對於第五部分並沒有進一步的提高。僅僅是多學習幾種模式可以解決更多的問題。
這裡的動態原型模式相對於第五部分的組合使用自定義構造函數模式和原型模式本質上是沒有什麼差別的,只是因為對於有其他OO(Object Oriented,面向對象)語言經驗的開發人員看到這種模式會覺得奇怪,因此我們可以將所有信息都封裝在構造函數中。本質上是通過檢測某個應該存在的方法是否存在或有效,來決定是否要初始化原型。如下例所示:
1 2 3 4 5 6 7 8 9 10 11 12 13function
Person(name,age,school){
this
.name=name;
this
.age=age;
this
.school=school;
if
(
typeof
this
.sayName !=
"function"
){
Person.prototype.sayName=
function
(){
console.log(
this
.name);
};
}
}
var
person=
new
Person(
"zzw"
,21,
"xjtu"
);
//使用new調用構造函數並創建一個實例對象
person.sayName();
//zzw
console.log(person.school);
//xjtu
這裡先聲明了一個構造函數,然後當使用new操作符調用構造函數創建實例對象時進入了構造函數的函數執行環境,開始檢測對象的sayName是否存在或是否是一個函數,如果不是,就使用原型修改的方式向原型中添加sayName函數。且由於原型的動態性,這裡所做的修改可以在所有實例中立即得到反映。值得注意的是,在使用動態原型模式時,不能使用對象字面量重寫原型,否則,在建立了實例的情況下重寫原型會導致切斷實例和新原型的聯系。
寄生構造函數模式是在前面幾種模式都不適用的情況下使用的。看以下例子,再做出說明:
1 2 3 4 5 6 7 8 9 10 11 12function
Person(name,age,school){
var
o =
new
Object();
o.name=name;
o.age=age;
o.school=school;
o.sayName=
function
(){
console.log(
this
.name);
};
return
o;
}
var
person =
new
Person(
"zzw"
,21,
"xjtu"
);
person.sayName();
//zzw
寄生構造函數的特點如下:
這個模式可以在特殊的情況下來為對象創建構造函數。假設我們想要創建一個具有額外方法的特殊數組,通過改變Array構造函數的原型對象是可以實現的,但是我在第四部分F中提到過,這種方式可能會導致後續的命名沖突等一系列問題,我們是不推薦的。而寄生構造函數就能很好的解決這一問題。如下所示:
1 2 3 4 5 6 7 8 9 10
function
SpecialArray(){
var
values=
new
Array();
values.push.apply(values,arguments);
values.toPipedString=
function
(){
return
this
.join(
"|"
);
};
return
values;
}
var
colors=
new
SpecialArray(
"red"
,
"blue"
,
"green"
);
console.log(colors.toPipedString());
//red|blue|green
或者如下所示:
1 2 3 4 5 6 7 8 9 10function
SpecialArray(string1,string2,string3){
var
values=
new
Array();
values.push.call(values,string1,string2,string3);
values.toPipedString=
function
(){
return
this
.join(
"|"
);
};
return
values;
}
var
colors=
new
SpecialArray(
"red"
,
"blue"
,
"green"
);
console.log(colors.toPipedString());
//red|blue|green
這兩個例子實際上是一樣的,唯一差別在於call()方法和apply()方法的應用不同。(這部分內容詳見《JavaScript函數之美~》)
這樣就既沒有改變Array構造函數的原型對象,又完成了添加Array方法的目的。
關於寄生構造函數模式,需要說明的是:返回的對象與構造函數或構造函數的原型屬性之間沒有任何關系;也就是說,構造函數返回的對象在與構造函數外部創建的對象沒有什麼不同。故不能依賴instanceof來確定對象類型。於是,我們建議在可以使用其他模式創建對象的情況下不使用寄生構造函數模式。
穩妥對象是指這沒有公共屬性,而且方法也不引用this的對象。穩妥對象適合在安全的環境中使用,或者在防止數據被其他應用程序改動時使用。舉例如下:
1 2 3 4 5 6 7 8 9function
Person(name,age,school){
var
o=
new
Object();
o.sayName=
function
(){
console.log(name);
};
return
o;
}
var
person=Person(
"zzw"
,21,
"xjtu"
);
person.sayName();
//zzw
可以看出來,這種模式和寄生構造函數模式非常相似,只是:
1.新創建對象的實例方法不用this。
2.不用new操作符調用構造函數(由函數名的首字母大寫可以看出它的確是一個構造函數)。
注意:變量person中保存的是一個穩妥對象,除了調用sayName()方法外沒有別的方式可以訪問其數據成員。例如在上述代碼下添加:
1 2console.log(person.name);
//undefined
console.log(person.age);
//uncefined
因此,穩妥構造函數模式提供的這種安全性,使得它非常適合在某些安全執行環境提供的環境下使用。
在這篇博文中,在創建大量相似對象的前提下,我以分析各種方法利弊的思路下向大家循序漸進地介紹了Object構造函數和對象字面量方法、工廠模式、自定義構造函數模式、原型模式、組合使用自定義構造函數模式和原型模式、動態原型模式、寄生構造函數模式、穩妥構造函數模式這幾種模式,其中我認為組合使用自定義構造函數模式和原型模式以及動態原型模式都是非常不錯的模式。而對於創建對象數量不多的情況下,對象字面量方法、自定義構造函數模式也都是不錯的選擇。
這一部分內容屬於JavaScript中的重難點,希望大家多讀幾遍,相信一定會有很大的收獲!
在寫這篇博文的過程中,設及知識點較多,錯誤在所難免,希望大家批評指正。