前言:
面向?qū)ο蟮恼Z言有一個(gè)標(biāo)志,那就是它們都有類的概念,而通過類可以創(chuàng)建任意多個(gè)具有相同屬性和方法的對(duì)象。ECMAScript中沒有類的概念,因此它的對(duì)象也與基于類的語言中的對(duì)象有所不同。
ECMA-262把對(duì)象定義為:“無序?qū)傩缘募?,其屬性可以包含基本值、?duì)象或者函數(shù)”。這就相當(dāng)于說對(duì)象是一組沒有特定順序的值。對(duì)象的每個(gè)屬性或方法都有一個(gè)名字,而每個(gè)名字都映射到一個(gè)值。正因?yàn)檫@樣,可以把ECMAScript的對(duì)象想象成散列表:無非就是一組名值對(duì),其中值可以是數(shù)據(jù)或函數(shù)。
每個(gè)對(duì)象都是基于一個(gè)引用類型創(chuàng)建的,這個(gè)引用類型可以是原生類型,也可以是開發(fā)人員定義的類型。
創(chuàng)建對(duì)象
1、 原始方法
創(chuàng)建自定義對(duì)象最簡(jiǎn)單的方式就是創(chuàng)建一個(gè)Object的實(shí)例,然后再為它添加屬性和方法,如下所示:
var person = new Object();
person.name = "xuguojun";
person.age = 24;
person.job = "前端工程師";
person.sayName = function(){
alert(this.name);
}
上面的例子創(chuàng)建了一個(gè)名為person的對(duì)象,并為它添加了三個(gè)屬性(name、age和job)和一個(gè)方法(sayName())。早期的時(shí)候經(jīng)常使用這個(gè)模式創(chuàng)建新對(duì)象。幾年后,對(duì)象字面量成為創(chuàng)建這種對(duì)象的首選。前面的例子用對(duì)象字面量語法可以寫成這樣:
var person = {
name: "xuguojun",
age: 24,
job: "前端工程師",
sayName: function(){
alert(this.name);
}
};
這個(gè)例子中的person對(duì)象與前面例子中的person對(duì)象是一樣的,都有相同的屬性和方法。這些屬性在創(chuàng)建時(shí)都帶有一些特征值,JavaScript通過這些特征值來定義它們的行為。
雖然Object構(gòu)造函數(shù)或?qū)ο笞置媪慷伎梢杂脕韯?chuàng)建代碼,但這些方式有一個(gè)明顯的缺點(diǎn):使用同一個(gè)接口創(chuàng)建很多對(duì)象,因?yàn)槌跏蓟闹刀际且恢碌臅?huì),所以會(huì)創(chuàng)建很多相似的對(duì)象,每個(gè)實(shí)例對(duì)象沒有自己的特性,會(huì)產(chǎn)生大量的重復(fù)代碼。
2、 工廠模式
工廠模式抽象了創(chuàng)建具體對(duì)象的過程,考慮到ECMAScript中無法創(chuàng)建類,開發(fā)人員就發(fā)明了一種函數(shù),用函數(shù)來封裝以特定接口創(chuàng)建對(duì)象的細(xì)節(jié)。其實(shí)現(xiàn)方法非常簡(jiǎn)單,也就是在函數(shù)內(nèi)創(chuàng)建一個(gè)對(duì)象,給對(duì)象賦予屬性及方法再將對(duì)象返回即可,如下所示:
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("Andy",29,"前端工程師");
var person2 = createPerson("xuguojun",24,"后端工程師");
函數(shù)createPerson()能夠根據(jù)接受的參數(shù)來構(gòu)建一個(gè)包含所有必要信息的person對(duì)象??梢詿o數(shù)次地調(diào)用這個(gè)函數(shù),而每次它都會(huì)返回一個(gè)包含三個(gè)屬性一個(gè)方法的對(duì)象。
工廠模式解決了創(chuàng)建多個(gè)相似對(duì)象的問題,但是工廠模式卻無從識(shí)別對(duì)象的類型,因?yàn)槿慷际荗bject,不像Date、Array等,因此出現(xiàn)了構(gòu)造函數(shù)模式。
3、 構(gòu)造函數(shù)模式
ECMAScript中的構(gòu)造函數(shù)可用來創(chuàng)建特定類型的對(duì)象。像Object和Array這樣的原生構(gòu)造函數(shù),在運(yùn)行時(shí)會(huì)自動(dòng)出現(xiàn)在執(zhí)行環(huán)境中。此外,也可以創(chuàng)建自定義的構(gòu)造函數(shù),從而定義自定義對(duì)象類型的屬性和方法。例如,可以使用構(gòu)造函數(shù)模式將前面的例子重寫如下:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Andy",29,"前端工程師");
var person2 = new Person("xuguojun",24,"后端工程師");
console.log(person1 instanceof Person); // true, 判斷person1是否是Person的實(shí)例,即解決了工廠模式中不能識(shí)別對(duì)象的類型的問題
這個(gè)例子與工廠模式中除了函數(shù)名不同以外,還有以下不同之處:
函數(shù)名首寫字母為大寫(雖然標(biāo)準(zhǔn)沒有嚴(yán)格規(guī)定首寫字母為大寫,但按照慣例,構(gòu)造函數(shù)的首寫字母用大寫)
沒有顯示的創(chuàng)建對(duì)象,直接將屬性和方法賦值給了this對(duì)象
沒有return語句
使用new創(chuàng)建對(duì)象
能夠識(shí)別對(duì)象(這正是構(gòu)造函數(shù)模式勝于工廠模式的地方)
函數(shù)名Person之所以使用的是大寫字母P,是因?yàn)榘凑諔T例構(gòu)造函數(shù)始終都應(yīng)該以一個(gè)大寫字母開頭,而非構(gòu)造函數(shù)則應(yīng)該以一個(gè)小寫字母開頭。主要是為了區(qū)別于ECMAScript中的其他函數(shù),因?yàn)闃?gòu)造函數(shù)本身也是函數(shù),只不過可以用來創(chuàng)建對(duì)象而已。
構(gòu)造函數(shù)與其他函數(shù)的唯一區(qū)別,就在于調(diào)用它們的方式不同。任何函數(shù)只要通過new操作符調(diào)用,那它就可以作為構(gòu)造函數(shù);而任何函數(shù),如果不通過new操作符來調(diào)用,那它跟普通函數(shù)一樣。
當(dāng)使用new操作符調(diào)用構(gòu)造函數(shù)時(shí),會(huì)經(jīng)歷以下四個(gè)步驟:
創(chuàng)建一個(gè)新對(duì)象;
將構(gòu)造函數(shù)的作用域賦給新對(duì)象(因此this就指向了這個(gè)新對(duì)象)
執(zhí)行構(gòu)造函數(shù)中的代碼(新對(duì)象初始化,為這個(gè)新對(duì)象添加屬性)
返回新對(duì)象
構(gòu)造函數(shù)雖然好用,但也并非沒有缺點(diǎn),使用構(gòu)造函數(shù)的最大的問題在于每次創(chuàng)建實(shí)例的時(shí)候都要重新創(chuàng)建一次方法(理論上每次創(chuàng)建對(duì)象的時(shí)候?qū)ο蟮膶傩跃煌?,而?duì)象的方法是相同的),然而創(chuàng)建兩次完全相同的方法是沒有必要的,因此,我們可以將函數(shù)移到對(duì)象外面。
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("Andy",29,"前端工程師");
var person2 = new Person("xuguojun",24,"后端工程師");
我們將sayName設(shè)置成全局函數(shù),這樣一來person1與person2訪問的都是同一個(gè)函數(shù),可是問題又來了,在全局作用域中定義了一個(gè)實(shí)際只想讓Person 使用的函數(shù),顯示讓全局作用域有些名副其實(shí),更讓人無法接受的是如果對(duì)象需要定義很多方法,那么就要定義很多個(gè)全局函數(shù) ,那么這個(gè)自定義的引用類型就毫無封裝性可言,因此可以通過原型來解決此問題。
4、 原型模式
我們創(chuàng)建的每個(gè)函數(shù)都有prototype(原型)屬性,這個(gè)屬性是一個(gè)指針,指向一個(gè)對(duì)象,而這個(gè)對(duì)象的用途是包含可以由特定類型的所有實(shí)例共享的屬性和方法。使用原型對(duì)象的好處就是可以讓所有對(duì)象實(shí)例共享它所包含的屬性及方法。換句話說,不必在構(gòu)造函數(shù)中定義對(duì)象實(shí)例的信息,而是可以把這些信息直接添加到原型對(duì)象中。
function Person(){
}
Person.prototype.name = "xuguojun";
Person.prototype.age = 24;
Person.prototype.job = "前端工程師";
Person.prototype.sayName = function() {
alert(this.name);
}
var person1 = new Person();
person1.sayName(); //"xuguojun"
var person2 = new Person();
person2.sayName(); //"xuguojun"
alert(person1.sayName == person2.sayName); //true
原型模式也不是沒有缺點(diǎn),首先,它省略了構(gòu)造函數(shù)傳遞初始化參數(shù)這一環(huán)節(jié),結(jié)果所有實(shí)例在默認(rèn)情況下都取得了相同的屬性值,這樣非常不方便。但這還是不是原型的最大問題,原型模式的最大問題在于共享的本性所導(dǎo)致的。
由于共享的本性,對(duì)于那些包含基本類型值的屬性倒還好,畢竟通過在實(shí)例上添加一個(gè)同名屬性,可以隱藏原型中的對(duì)應(yīng)屬性;可如果屬性值是引用類型的話,問題就比較突出了,因?yàn)槿绻粋€(gè)實(shí)例修改了引用,則修改會(huì)在另一個(gè)實(shí)例中反映出來,如下所示:
function Person(){
}
Person.prototype.name = "xuguojun";
Person.prototype.age = 24;
Person.prototype.job = "前端工程師";
Person.prototype.friends = ['Andy','Alen']
Person.prototype.sayName = function() {
alert(this.name);
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push('Tom');
alert(person1.friends); //Andy, Alen, Tom
alert(person2.friends); //Andy, Alen, Tom
alert(person1.friends == person2.friends); //true
因此我們通常不單獨(dú)使用原型,而是結(jié)合原型模式與構(gòu)造函數(shù)模式。
5、 混合模式(原型模式 + 構(gòu)造函數(shù)模式)
創(chuàng)建自定義對(duì)象類型的最常見方式,就是組合使用構(gòu)造函數(shù)模式與原型模式。構(gòu)造函數(shù)模式用于定義實(shí)例屬性,而原型模式用于定義方法和共享的屬性。結(jié)果,每個(gè)實(shí)例都會(huì)有自己的一份實(shí)例屬性的副本,但同時(shí)又共享者著對(duì)方法的引用,最大限度的節(jié)省了內(nèi)存。另外,這種模式還支持向構(gòu)造函數(shù)傳遞參數(shù)。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['Andy','Alen'];
}
Person.prototype = {
constructor : Person,
sayName : function() {
alert(this.name);
}
}
var person1 = new Person("xuguojun",24,"前端工程師");
var person2 = new Person("Grry",25,"后端工程師");
person1.friends.push('vivian');
alert(person1.friends); //Andy,Alen,vivian
alert(person2.friends); //Andy,Alen
alert(person1.friends == person2.friends); //false
alert(person1.sayName == person2.sayName); //true
這種構(gòu)造函數(shù)與原型混成的模式,是目前在ECMAScript中是使用最廣泛、認(rèn)同度最高的一種創(chuàng)建自定義對(duì)象的方法??梢哉f,這是用來定義引用類型的一種默認(rèn)模式。
6、 單例模式
在傳統(tǒng)開發(fā)工程師眼里,單例就是保證一個(gè)類只有一個(gè)實(shí)例,實(shí)現(xiàn)的方法一般是先判斷實(shí)例存在與否,如果存在直接返回,如果不存在就創(chuàng)建了再返回,這就確保了一個(gè)類只有一個(gè)實(shí)例對(duì)象。在JavaScript里,單例作為一個(gè)命名空間提供者,從全局命名空間里提供一個(gè)唯一的訪問點(diǎn)來訪問該對(duì)象。
var People = (function(){
var instance;
function init(name) {
return {
name:name
};
}
return {
createPeople: function(name) {
if (!instance) {
instance = init(name);
}
return instance;
}
};
}());
People.createPeople('xuguojun'); //{name:'xuguojun'}
People.createPeople('Andy'); //{name:'xuguojun'}
單例模式實(shí)例:
實(shí)現(xiàn)功能:每次點(diǎn)擊按鈕1和按鈕2,都會(huì)加1,值在對(duì)應(yīng)按鈕下面的result is顯示;當(dāng)點(diǎn)擊完按鈕1在點(diǎn)擊按鈕2時(shí),會(huì)在按鈕1值的基礎(chǔ)上加1,值在按鈕2下面的result is顯示,反過來亦是如此。
7、 模塊模式
現(xiàn)在web工程中,javascript所占的比重越來越多。越來越重要。相應(yīng)的,前臺(tái)所寫的代碼也越來越多。代碼約多,就越不好管理,一個(gè)項(xiàng)目中多人開發(fā),如果出現(xiàn)同名的變量怎么辦?這就需要我們的模塊模式。
模塊是任何強(qiáng)大應(yīng)用程序架構(gòu)中不可或缺的一部分,它通常能夠幫我們清晰的分離和組織項(xiàng)目中的代碼單元。模塊模式的思路是為對(duì)象添加私有變量和私有方法,同時(shí)又要減少全局變量的使用。
var Person = (function() {
// 創(chuàng)建私有變量
var name = "xuguojun";
// 創(chuàng)建私有函數(shù)
function sayName() {
console.log(name);
};
// 返回一個(gè)對(duì)象包含公有方法和屬性
return {
name: name,
sayName: sayName
}
})()
模塊模式使用了一個(gè)返回對(duì)象的匿名函數(shù)。在這個(gè)匿名函數(shù)內(nèi)部,先定義了私有變量和函數(shù),供內(nèi)部函數(shù)使用,然后將一個(gè)對(duì)象字面量作為函數(shù)的值返回,返回的對(duì)象字面量中只包含可以公開的屬性和方法。這樣的話,可以提供外部使用該方法;由于該返回對(duì)象中的公有方法是在匿名函數(shù)內(nèi)部定義的,因此它可以訪問內(nèi)部的私有變量和函數(shù)。
什么時(shí)候使用模塊模式?
如果我們必須創(chuàng)建一個(gè)對(duì)象并以某些數(shù)據(jù)進(jìn)行初始化,同時(shí)還要公開一些能夠訪問這些私有數(shù)據(jù)的方法,那么我們這個(gè)時(shí)候就可以使用模塊模式了。
增強(qiáng)的模塊模式:
增強(qiáng)的模塊模式的使用場(chǎng)合是:適合那些單列必須是某種類型的實(shí)例,同時(shí)還必須添加某些屬性或方法對(duì)其加以增強(qiáng)的情況。比如如下代碼:
function CustomType() {
this.name = "tugenhua";
};
CustomType.prototype.getName = function(){
return this.name;
}
var application = (function(){
// 定義私有變量
var privateA = "aa";
// 定義私有函數(shù)
function A(){};
// 實(shí)例化一個(gè)對(duì)象后,返回該實(shí)例,然后為該實(shí)例增加一些公有屬性和方法
var object = new CustomType();
// 添加公有屬性
object.A = "aa";
// 添加公有方法
object.B = function(){
return privateA;
}
// 返回該對(duì)象
return object;
})();
打印下application對(duì)象:

繼續(xù)打印該公有屬性和方法如下:
console.log(application.A);// aa
console.log(application.B()); // aa
console.log(application.name); // tugenhua
console.log(application.getName());// tugenhua
8、 發(fā)布訂閱模式
發(fā)布訂閱模式定義對(duì)象間的一種一對(duì)多的依賴關(guān)系,當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對(duì)象都將得到通知。在 JavaScript 開發(fā)中,我們一般用事件模型來替代傳統(tǒng)的發(fā)布—訂閱模式,只要我們?cè)?jīng)在 DOM 節(jié)點(diǎn)上面綁定過事件函數(shù),那我們就曾經(jīng)使用過發(fā)布訂閱模式。
發(fā)布訂閱模式可以廣泛應(yīng)用于異步編程中,這是一種替代傳遞回調(diào)函數(shù)的方案。比如,我們可以訂閱 ajax 請(qǐng)求的 error 、 succ 等事件。 或者如果想在動(dòng)畫的每一幀完成之后做一些事情,那我們可以訂閱一個(gè)事件,然后在動(dòng)畫的每一幀完成之后發(fā)布這個(gè)事件。在異步編程中使用發(fā)布—訂閱模式,我們就無需過多關(guān)注對(duì)象在異步運(yùn)行期間的內(nèi)部狀態(tài),而只需要訂閱感興趣的事件發(fā)生點(diǎn)。
發(fā)布訂閱模式可以取代對(duì)象之間硬編碼的通知機(jī)制,一個(gè)對(duì)象不用再顯式地調(diào)用另外一個(gè)對(duì)象的某個(gè)接口。發(fā)布—訂閱模式讓兩個(gè)對(duì)象松耦合地聯(lián)系在一起,雖然不太清楚彼此的細(xì)節(jié),但這不影響它們之間相互通信。當(dāng)有新的訂閱者出現(xiàn)時(shí),發(fā)布者的代碼不需要任何修改;同樣發(fā)布者需要改變時(shí),也不會(huì)影響到之前的訂閱者。只要之前約定的事件名沒有變化,就可以自由地改變它們。
var EventCenter = (function(){
var events = {};
function on(evt, handler){
events[evt] = events[evt] || [];
events[evt].push({
handler: handler
});
}
function fire(evt, args){
if(!events[evt]){
return;
}
for(var i=0; i<events[evt].length; i++){
events[evt][i].handler(args);
}
}
function off(name) {
delete events[name];
}
return {
on: on, // 訂閱
fire: fire, // 發(fā)布
off: off //取消訂閱
}
})();
EventCenter.on('my_event', function(data){
console.log('my_event received...');
});
EventCenter.fire('my_event'); // my_event received...
EventCenter.off('my_event');
EventCenter.fire('my_event'); //undefined
使用場(chǎng)景:
-
應(yīng)用于異步編程,替代傳統(tǒng)回調(diào)。
用它的好處是可以切換我們的關(guān)注點(diǎn),關(guān)注點(diǎn)集中在訂閱事件,而在異步回調(diào)中我們需要關(guān)注內(nèi)部運(yùn)行狀態(tài)。
-
取代對(duì)象之間的硬編碼機(jī)制,對(duì)象之間不必再顯式調(diào)用。
優(yōu)點(diǎn)是對(duì)象間達(dá)到松耦合,缺點(diǎn)是當(dāng)有多個(gè)發(fā)布者和訂閱者嵌套時(shí),極難debug。
發(fā)布訂閱模式實(shí)例:
實(shí)現(xiàn)功能:對(duì)點(diǎn)擊次數(shù)累加求和,并記錄當(dāng)前點(diǎn)擊次數(shù)。如果點(diǎn)擊了5次,則會(huì)計(jì)算result is=1+2+3+4+5。