了解并掌握各種JavaScript用于創(chuàng)建自定義類型對象的設(shè)計(jì)模式有利于幫助我們認(rèn)識它們各自的優(yōu)缺點(diǎn)和適用場景,這樣我們在今后的開發(fā)過程中才能夠做到有的放矢涩笤,在正確的場合使用正確的模式創(chuàng)建對象嚼吞。
一盒件、單例模式
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function() {
alert(this.name);
};
單例模式是指通過創(chuàng)建一個Object
對象,并為其設(shè)置各種屬性和方法舱禽,以滿足自定義對象的使用需求炒刁,如上所示;很明顯誊稚,上面的多條語句顯得十分分散翔始,為了更好地將它們組合起來,更好的辦法是使用對象字面量創(chuàng)建:
var person = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function() {
alert(this.name);
}
};
模式評價:雖然Object構(gòu)造函數(shù)或?qū)ο笞置媪慷伎梢杂脕韯?chuàng)建單個對象里伯,但這些方式有兩個明顯的缺點(diǎn):
- 沒有做到代碼的復(fù)用城瞎,即要是使用同樣的辦法創(chuàng)建多個對象,會產(chǎn)生大量的重復(fù)代碼疾瓮。
- 創(chuàng)建出的對象沒有具體的類型脖镀,它們只是
Object
類型的一個實(shí)例。
二狼电、工廠模式
為了解決單例模式的代碼復(fù)用問題认然,更好的辦法是采用工廠模式:
function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
alert(this.name);
};
return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
由于JavaScript中無法創(chuàng)建類,所以人們就發(fā)明了一種函數(shù)漫萄,用函數(shù)來封裝以特定接口創(chuàng)建對象的細(xì)節(jié),這就是工廠模式的設(shè)計(jì)原理盈匾。
在上面的例子中腾务,函數(shù)createPerson()
能夠根據(jù)接受的參數(shù)來構(gòu)建一個包含所有必要信息的Person
對象,并且可以無數(shù)次地調(diào)用這個函數(shù)削饵,而每次調(diào)用都會返回一個包含三個屬性和一個方法的對象岩瘦。
模式評價:工廠模式雖然解決了創(chuàng)建多個相似對象造成的代碼重復(fù)問題,但仍未解決對象類型的區(qū)分問題(怎樣知道一個對象的具體類型)窿撬;通過工廠模式創(chuàng)建出的對象启昧,其類型都是Object
,如果能把上例中創(chuàng)建出的對象標(biāo)記為Person
類型就好了劈伴。
同時密末,通過工廠模式創(chuàng)建出的對象還存在著一個很嚴(yán)重的問題,那就是內(nèi)存浪費(fèi)跛璧。每當(dāng)調(diào)用一次createPerson()
函數(shù)創(chuàng)建一個對象严里,就會在其內(nèi)部創(chuàng)建一個函數(shù)實(shí)例。在前面的例子中追城,person1
和person2
都有一個名為sayName()
的方法刹碾,但person1.sayName()
和person2.sayName()
并不是引用的同一個函數(shù)實(shí)例,而是不同的實(shí)例座柱,因?yàn)?/p>
o.sayName = function() {
alert(this.name);
};
與
o.sayName = new Function("alert(this.name)");
在邏輯上是完全等價的迷帜。為了證明person1.sayName()
和person2.sayName()
引用的是不同的函數(shù)實(shí)例物舒,有:
alert(person1.sayName == person2.sayName) //false
因此,若一個自定義對象類型中要是有多個方法戏锹,那么通過工廠模式定義出多個對象造成的內(nèi)存浪費(fèi)就可想而知了冠胯。
三、構(gòu)造函數(shù)模式
為了解決對象的類型問題景用,可以使用構(gòu)造函數(shù)模式涵叮,JavaScript中的構(gòu)造函數(shù)可以用來創(chuàng)建特定類型的對象:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
首先需要明確什么樣的函數(shù)稱為構(gòu)造函數(shù),即通過new
操作符+函數(shù)名的方式來創(chuàng)建對象的函數(shù)伞插,就叫做構(gòu)造函數(shù)割粮。
構(gòu)造函數(shù)創(chuàng)建對象實(shí)例的過程:
- 創(chuàng)建一個新對象;
- 把當(dāng)前構(gòu)造函數(shù)內(nèi)的作用域賦給新創(chuàng)建的這個對象(此時構(gòu)造函數(shù)內(nèi)部的this指針就指向了這個新對象)媚污;
- 執(zhí)行構(gòu)造函數(shù)中的代碼舀瓢;
- 返回新對象
同時構(gòu)造函數(shù)還有一個極其重要的特性:默認(rèn)情況下,若構(gòu)造函數(shù)內(nèi)部沒有通過return
語句返回其它類型的變量或?qū)ο蠛拿溃瑒t通過構(gòu)造函數(shù)創(chuàng)建并返回的對象其類型由構(gòu)造函數(shù)名指定京髓。
這個特性的意思就是,在上例中商架,通過名為Person
的構(gòu)造函數(shù)創(chuàng)建出的person1
和person2
對象實(shí)例堰怨,其對象類型都是Person
;也就是說蛇摸,它們都是Person
類型的對象實(shí)例(可以使用instanceof
檢驗(yàn)):
alert(person1 instanceof Person); //true
alert(person2 instanceof Person); //true
正因?yàn)檫@個特性备图,所以才為什么說構(gòu)造函數(shù)可以用來創(chuàng)建特定類型的對象。
這樣一來赶袄,構(gòu)造函數(shù)模式就很好理解了:
- 通過
new Person()
調(diào)用Person
構(gòu)造函數(shù)揽涮,創(chuàng)建出一個Person
類型的對象。 - 將
Person
構(gòu)造函數(shù)內(nèi)部的作用域賦給該對象饿肺,即此時函數(shù)內(nèi)部的this指向了該對象蒋困。 - 執(zhí)行構(gòu)造函數(shù)代碼,通過this為該對象創(chuàng)建屬性和方法敬辣。
- 由于
Person
構(gòu)造函數(shù)內(nèi)部沒有用return
語句返回其它變量和對象雪标,所以Person
構(gòu)造函數(shù)返回該對象。
進(jìn)一步優(yōu)化:
為了解決工廠模式提到的內(nèi)存浪費(fèi)問題溉跃,我們發(fā)現(xiàn)創(chuàng)建兩個完成同樣任務(wù)的Function
實(shí)例的確沒有必要汰聋;況且有this對象在,根本不用在執(zhí)行代碼前就把函數(shù)綁定到特定對象上喊积。因此烹困,可以把公共函數(shù)的定義轉(zhuǎn)移到構(gòu)造函數(shù)外部:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
};
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
模式評價:
構(gòu)造函數(shù)模式有效地解決了對象的類型問題,是比工廠模式更佳的解決方案乾吻。同時為了解決工廠模式中遇到的內(nèi)存浪費(fèi)問題髓梅,選擇將公共函數(shù)的定義轉(zhuǎn)移到構(gòu)造函數(shù)的外部拟蜻,看樣子解決了目前遇到的所有問題。然而新的問題又來了:把公共函數(shù)定義在全局作用域中枯饿,而僅僅只是為了供對象調(diào)用酝锅,看起來似乎有些小題大做了。而更讓人感到違和的是奢方,如果一個對象有很多公共函數(shù)搔扁,或者把所有對象的公共函數(shù)定義都放在全局作用域中,暫且不說我們這個自定義的引用類型毫無封裝性可言蟋字,更有可能會與全局函數(shù)的定義混在一起難以區(qū)分稿蹲,從而增加了代碼的維護(hù)成本。因此我們?nèi)孕枰乙粋€具有更好封裝性的解決方案鹊奖。
四苛聘、原型模式
我們創(chuàng)建的每個函數(shù)都有一個prototype
(原型)屬性,這個屬性是一個指針,指向一個對象(原型對象)忠聚,而這個對象的用途是可以包含由特定類型的所有實(shí)例共享的屬性和方法设哗。因此產(chǎn)生了原型模式:
function Person() {
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //Nicholas
var person2 = new Person();
person2.sayName(); //Nicholas
alert(person1.sayName == person2.sayName) //true
由于原型對象的存在,我們可以在構(gòu)造函數(shù)中什么都不寫两蟀,只要是通過構(gòu)造函數(shù)創(chuàng)建的對象實(shí)例(new
操作符)网梢,都能夠與函數(shù)的原型對象相關(guān)聯(lián),從而訪問原型對象中的屬性和方法赂毯。這也是為什么person1.sayName == person2.sayName
战虏,因?yàn)樗鼈冊L問的屬性和方法都是位于原型對象中的同一份。
每當(dāng)創(chuàng)建一個函數(shù)時欢瞪,原型對象會自動獲得一個屬性:constructor
,該屬性指向prototype
所在函數(shù)徐裸,即在本例中遣鼓,Person.prototype.constructor == Person
。當(dāng)利用構(gòu)造函數(shù)創(chuàng)建一個對象實(shí)例時重贺,創(chuàng)建出的對象會自動獲得一個指向當(dāng)前構(gòu)造函數(shù)原型對象的內(nèi)部指針[[prototype]]
(雖然無法訪問但卻是真實(shí)存在的)骑祟,這也解釋了為什么所有通過構(gòu)造函數(shù)創(chuàng)建的實(shí)例能夠訪問同一個原型對象中的屬性和方法。
再看JavaScript引擎是如何搜索某個對象的某個屬性的:每當(dāng)代碼讀取某個對象的某個屬性時气笙,都會執(zhí)行一次搜索次企,目標(biāo)是具有給定名字的屬性。搜索先從對象實(shí)例本身開始潜圃。如果在實(shí)例中找到了具有給定名字的屬性缸棵,則返回該屬性的值;否則繼續(xù)搜索
[[prototype]]
內(nèi)部指針指向的原型對象谭期,在原型對象中查找具有給定名字的屬性堵第。如果在原型對象中找到了該屬性吧凉,則返回原型對象中的該屬性值。
原型模式利用了所有對象實(shí)例訪問同一個原型對象的機(jī)制來解決內(nèi)存浪費(fèi)問題踏志,不過在前面的例子中阀捅,每添加一個屬性和方法都要敲一遍Person.prototype
。為減少冗余代碼针余,也為視覺上更好的封裝性饲鄙,更常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象:
function Person() {
}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
然而這樣又會帶來一個問題,那就是利用對象字面量來重寫Person.prototype
后圆雁,Person.prototype.constructor
已經(jīng)不再指向Person
了忍级;這是因?yàn)槔脤ο笞置媪縿?chuàng)建出的對象是Object()
構(gòu)造函數(shù)創(chuàng)建出的實(shí)例,其prototype.constructor
指向的是Object()
構(gòu)造函數(shù)摸柄;因此颤练,如果constructor
十分重要的話,還需要將其顯式設(shè)置回原來的值:
function Person() {
}
Person.prototype = {
construct: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
模式評價:基于原型對象的特點(diǎn)驱负,原型模式可以將自定義類型的方法定義在原型對象內(nèi)部嗦玖,而不用再將其放到全局作用域中,因此從這一點(diǎn)考慮跃脊,它是比構(gòu)造函數(shù)模式更優(yōu)的解決方案宇挫;然而原型模式也并非沒有缺點(diǎn),那就是它省略了為構(gòu)造函數(shù)傳遞初始化參數(shù)的這一環(huán)節(jié)酪术,結(jié)果使得所有實(shí)例在默認(rèn)情況下都取得相同的屬性值器瘪,每個對象無法擁有自己獨(dú)特的屬性內(nèi)容,這顯然是與“利用相同模板創(chuàng)建不同對象”的理念背道而馳的绘雁。因此這也正是很少看到有人單獨(dú)使用原型模式創(chuàng)建對象的原因所在橡疼。
五、組合使用構(gòu)造函數(shù)模式和原型模式
為了解決原型模式所遇到的困境庐舟,自然而然會想到利用原型對象定義公共方法欣除,而把共享的屬性放到構(gòu)造函數(shù)中的方式來創(chuàng)建對象,而這正是組合使用構(gòu)造函數(shù)模式和原型模式的思路:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
}
Person.prototype = {
constructor: Person,
sayName: function() {
alert(this.name);
}
};
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
這樣一來挪略,每個對象都有自己的屬性历帚,但同時又能共享一份相同的方法,最大限度地節(jié)約了內(nèi)存杠娱。
模式評價:這種混成模式集構(gòu)造函數(shù)模式和原型模式各自之長挽牢,是目前JavaScript中使用最廣泛、認(rèn)同度最高的一種創(chuàng)建自定義類型的方法摊求∏莅危可以說,這是用來定義引用類型的一種默認(rèn)模式。
六奏赘、動態(tài)原型模式
即便是已經(jīng)擁有了如此好的用來定義引用類型的設(shè)計(jì)模式寥闪,但習(xí)慣于使用Java/C++等語言的開發(fā)人員可能仍會認(rèn)為:組合使用構(gòu)造函數(shù)模式和原型模式的設(shè)計(jì)模式感覺還是和單獨(dú)使用構(gòu)造函數(shù)模式差不多,前者雖然比后者好那么一些磨淌,但還是沒法做到徹底地封裝疲憋,畢竟構(gòu)造函數(shù)和原型對象依然是分開定義的,從這一點(diǎn)來說梁只,兩者并沒有多大差別缚柳。為了做到將兩者結(jié)合到一起,實(shí)現(xiàn)徹底的封裝搪锣,于是就有了動態(tài)原型模式:
function Person(name, age, job) {
//屬性
this.name = name;
this.age = age;
this.job = job;
//方法
if(typeof this.sayName != "function") {
//所有的公有方法都在這里定義
Person.prototype.sayName = function() {
alert(this.name);
}秋忙;
Person.prototype.sayJob = function() {
alert(this.job);
};
Person.prototype.sayAge = function() {
alert(this.age);
};
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); //Nicholas
person2.sayName(); //Greg
動態(tài)原型模式把所有信息都封裝到了構(gòu)造函數(shù)中,既通過在構(gòu)造函數(shù)中初始化原型(只會在第一次調(diào)用構(gòu)造函數(shù)創(chuàng)建對象時進(jìn)行)构舟,也保持了同時使用構(gòu)造函數(shù)和原型的優(yōu)點(diǎn)灰追。
我們可以分析一下該模式創(chuàng)建對象實(shí)例的過程:
- 首先通過
new Person()
調(diào)用構(gòu)造函數(shù),此時立刻創(chuàng)建了一個Person
類型的對象狗超。 - 新創(chuàng)建的對象獲得了指向構(gòu)造函數(shù)原型對象的指針弹澎,此時的原型對象中只有
constructor
屬性,且指向Person
函數(shù)努咐。 - 將
Person
構(gòu)造函數(shù)內(nèi)部的作用域賦給該對象苦蒿,即此時函數(shù)內(nèi)部的this指向了該對象。 - 進(jìn)入構(gòu)造函數(shù)執(zhí)行內(nèi)部語句渗稍,首先設(shè)置新對象的
name
,age
,job
屬性 - 隨后搜索并判斷新對象中是否已存在有效的
sayName()
函數(shù)佩迟,如果不存在,說明原型對象中還沒有添加該方法竿屹,此時創(chuàng)建原型對象中的對應(yīng)方法报强。 - 由于新對象中保存的是指向原型對象的指針,所以在原型對象中添加的方法能被新對象動態(tài)訪問到
- 返回新對象并退出構(gòu)造函數(shù)
構(gòu)造函數(shù)中通過檢查某個應(yīng)該存在的方法是否有效拱燃,來決定是否需要初始化原型秉溉;這是一個非常巧妙的策略。若把所有需要初始化的公共方法和公共屬性都放在一起扼雏,僅僅只需要檢查其中一個即可知道是否已執(zhí)行過這段代碼坚嗜。因此在上例中夯膀,只有第一次創(chuàng)建對象person1
時才會初始化原型诗充,而當(dāng)創(chuàng)建對象person2
時,由于原型已經(jīng)初始化了诱建,所以將不再重復(fù)此過程蝴蜓。
不過需要特別注意的是,該模式下不能使用對象字面量構(gòu)造原型,如下面這樣:
function Person(name, age, job) {
//屬性
this.name = name;
this.age = age;
this.job = job;
//方法
if(typeof this.sayName != "function") {
//不能用這樣的方法初始化原型
Person.prototype = {
sayName: function() {
alert(this.name);
},
sayJob: function() {
alert(this.job);
},
sayAge: function() {
alert(this.age);
}
};
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); //出錯
person2.sayName(); //出錯
這是因?yàn)椋舭言蛯ο蟮某跏蓟^程放到構(gòu)造函數(shù)中茎匠,那么當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建對象時格仲,對象的創(chuàng)建總是先于原型對象的初始化的(在執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼前對象就已經(jīng)完成了創(chuàng)建),由于對象獲得指向原型對象的指針發(fā)生在對象創(chuàng)建時刻诵冒,所以在這種情況下凯肋,創(chuàng)建的對象早已指向了原型對象,對于之前的情況汽馋,由于Person.prototype
和新對象的[[prototype]]
指向同一個對象侮东,所以通過Person.prototype
修改原型對象能被新對象的[[prototype]]
訪問到;然而使用對象字面量時豹芯,Person.prototype
通過賦值的方式指向了另一個對象悄雅,但此時[[prototype]]
仍指向初始的原型對象,這也是為什么person1.sayName()
出錯的原因铁蹈。
總結(jié):
通過對各個模式的分析發(fā)現(xiàn)宽闲,組合使用構(gòu)造函數(shù)模式和原型模式與動態(tài)原型模式是所有介紹過的模式中最適合用來創(chuàng)建自定義類型對象的兩個模式。這兩者雖然從本質(zhì)上看是相同的握牧,但是由于實(shí)現(xiàn)細(xì)節(jié)的不同使得它們互有優(yōu)勢容诬。對于前者來說,它能夠使用對象字面量的方式構(gòu)造原型對象我碟,適用于定義具有較多公共方法的對象類型放案,這樣可以簡化代碼,但其構(gòu)造函數(shù)與原型對象是分開定義的矫俺,封裝性一般吱殉。而后者正好與之相反,后者雖不能使用對象字面量創(chuàng)建原型對象厘托,但卻做到了將原型對象的初始化封裝到了構(gòu)造函數(shù)中從而形成一個整體友雳,這樣更加符合OO的思想。