javascript有很多創(chuàng)建對象的模式,完成工作的方式也不只一種。你可以隨時定義自己的類型或自己的泛用對象??梢允褂美^承或混入等其他技術(shù)令對象間行爲(wèi)共享。也可以利用javascript高級技巧來阻止對象結(jié)構(gòu)被改變。本章討論的模式賜予你強大的管理和創(chuàng)建對象的能力,完全基於你自己的用例。
6.1 私有成員和特權(quán)成員
javascript對象的多有屬性都是公有的,且沒有顯式的方法指定某個屬性不嗯你被外界某個對象訪問。然而,有時你可能不惜我數(shù)據(jù)公有。例如,當(dāng)一個對象使用一個值來決定某種狀態(tài),在對象不知情的情況下修改該值會讓狀態(tài)管理變得混亂。一種避免它的方法時通過使用命名規(guī)則。例如,在不希望公有的屬性名字加上下劃線(如this._name)。還有很多其他方法不需要依賴命名規(guī)則,因此在阻止私有信息被修改方面也就更加“防彈”。
6.6.1 模塊模式
模塊模式是一種用於創(chuàng)建擁有私有數(shù)據(jù)的單件對象的模式?;咀龇〞r時喲還能夠里調(diào)函數(shù)表達(IIFE)來返回一個對象。IIFE是一種被定義後立即調(diào)用並產(chǎn)生結(jié)果的函數(shù)表達,該函數(shù)表達可以包括任意數(shù)量的本地變量,它們在函數(shù)外不可見。因爲(wèi)返回的對象被定義在函數(shù)內(nèi)部,對象的方法可以訪問這些數(shù)據(jù)。(IIFE定義的所有對象都可以訪問同樣的本地變量)以這種方式訪問私有數(shù)據(jù)的方式被稱爲(wèi)特權(quán)方法。下面時模塊模式的基本格式。
var yourObject=(function(){
//private data variable
return{
//public methods and properties
}
}())
該模式創(chuàng)建來一個匿名函數(shù)並立即執(zhí)行。注意在函數(shù)尾部有額外的小括號,你可以用這種語法立刻執(zhí)行匿名函數(shù)。這意味著這個函數(shù)僅存在於被調(diào)用的瞬間,一旦執(zhí)行後立即就被銷毀。IIFE是javascript一種非常流行的模式,部分原因就是它在模塊模式中的應(yīng)用。
模塊模式允許你使用普通變量作爲(wèi)非公有對象屬性。通過創(chuàng)建閉包函數(shù)作爲(wèi)對象方法來操作它們。閉包函數(shù)就是一個可以訪問其作用於外部數(shù)據(jù)的普通函數(shù)。舉例來說,當(dāng)你在一個函數(shù)中訪問一個全局對象,比如網(wǎng)頁瀏覽器中的window,該函數(shù)就是在訪問其作用域外的變量。區(qū)別是,在模塊模式中,變量定義在IIFE中,而訪問變量的函數(shù)也定義在IIFE中。
var person =(function(){
var age=25;
return{
name:"Nicholas",
getAge:function(){
return age;
},
growOlder:function(){
age++;
}
};
}());
console.log(person.name);
console.log(person.getAge());
person.age=100;
console.log(person.getAge());
person.growOlder();
console.log(person.getAge());
這段代碼使用模塊模式創(chuàng)建了person對象。變量age就是該對象的一個私有屬性。它無法被外界直接訪問,但可以通過對象方法來操作。該對象上有兩個特權(quán)方法:getAge()讀取變量age的值,growOlder()讓age自增。這兩個方法都可以直接訪問age,因爲(wèi)它們都定義在一個IIFE裏面。
模塊模式還有一個變種叫暴露模塊模式,它將所有的變量和方法都組織在IIFE頂部,然後將它們設(shè)置到需要被返回的對象上。你可以用暴露模塊模式改寫前例,如下。
var person=(function(){
var age =25;
function getAge(){
return age;
}
function growOlder(){
age++;
}
return{
name:"Nicholas",
getAge:getAge,
growOlder:growOlder
};
}());
在暴露模塊模式中,age,getAge()和growOlder()都被定義成IIFE的本地對象。然後getAge()和growOlder()函數(shù)都被設(shè)置到返回的對象中,有效地對外界暴露來它們。這段代碼和使用傳統(tǒng)模塊模式的前例一模一樣;然而,有人更喜歡這種模式,因爲(wèi)它們保證所有的變量和函數(shù)聲明都在一處。
6.1.2 構(gòu)造函數(shù)的私有成員
模塊模式在定義單個對象的私有屬性上十分有效,但對於那些同樣需要私有屬性的自定義類型又如何呢?你可以在構(gòu)造函數(shù)中使用類似的模式來創(chuàng)建每個實例的私有數(shù)據(jù)。
function person(name){
//define a variable only accessible inside of ?the Person constructor
var age =25;
this.name=name;
this.getAge=function(){
return age;
};
this.growOlder=function(){
age++;
};
}
var person=new Person("Nicholas");
console.log(person.name);
console.log(person.getAge());
person.age=100;
console.log(person.getAge());
person.growOlder();
console.log(person.getAge());
在這段代碼中,Person構(gòu)造函數(shù)有一個本地變量age。該變量被用於getAge()和growOlder()方法。當(dāng)你創(chuàng)建Person的一個實例時。該實例接受其自身的age變量。getAge()方法和growOlder()方法。這種做法在很多方面都類似模塊模式,構(gòu)造函數(shù)創(chuàng)建一個本地作用域並返回this對象。在第四章討論過,將方法直接放在對象的實例上不如放在其原型對象上有效,但如果你絮語奧實例私的數(shù)據(jù),這是唯一可行的手段。
如果你需要所有實例可共享私有數(shù)據(jù),可以結(jié)合模塊模式和構(gòu)造函數(shù)。如下
var Person=(function(){
//everyone shares the same age
var age=25;
function InnerPerson(name){
this.name=name;
}
InnerPerson.prototype.getAge=function(){
return age;
};
InnerPerson.prototype.growOlder=function(){
age++;
};
return InnerPerson;
}());
var person1=new Person("Nicholas");
var person2=new Person("Greg");
console.log(person.name);
console.log(person1.getAge());
在這段代碼中,InnerPerson構(gòu)造函數(shù)被定義在一個IIFE中。變量age被定義在構(gòu)造函數(shù)外並被兩個原型對象的方法使用。IIFE返回InnerPerson構(gòu)造函數(shù)作爲(wèi)全局作用域里的Person構(gòu)造函數(shù)。最終,Person的全部實例得以共享age變量,所以在一個實例上的改變自動影響了另一個。
6.2 混入
Javascript中大量使用來味蕾繼承和原型對象繼承,還有另一種僞繼承的手段叫混入。一個對象在不改變原型對象鏈的情況下得到來另一個對象屬性的手段叫混入。第一個對象(接受者)同構(gòu)直接複製第二個對象(提供者)的屬性從而接收 了這些屬性。下面是傳統(tǒng)的利用函數(shù)實現(xiàn)的混入。
function mixing(receiver,supplier){
for(var property in supplier){
if(supplier.hasOwnProperty(property)){
receiver[property]=supplier[property]}
}}
return receiver;
函數(shù)mixin()接受兩個參數(shù):接受者和提供者。該函數(shù)的目的將提供者所有的可枚舉的屬性賦值給接受者??梢酝ㄟ^使用for-in循環(huán)迭代提供者的屬性並將值設(shè)置給接受者的同名屬性達成這一目的。記住這是淺拷貝,所有如果屬性內(nèi)包含的時一個對象,那麼提供者和接受者將指向同一個對象。這個模式被廣泛用於將一個javascript對象內(nèi)已經(jīng)存在的行爲(wèi)添加到另一個對象中去。
假如,可以通過混入而不是繼承給一個對象添加事件支持。首先,假設(shè)你已經(jīng)有一個支持事件的自定義類型。
function EventTarget(){}
EventTarget.prototype={
constructor:EventTarget,
addListener:function(type,listener){
//create an array if it doesn't exist
if(!this.hasOwnProperty("_listeners")){
this._listeners[type]=[];}
this._listeners[type].push(listener);
},
fire:function(event){
if(!event.target){
event.target=this;}
if(!event.type){
throw new Error("Event object missing 'type' property ");
}
if(this._listeners&&this._listeners[event.type] instanceof Array){
var listeners =this._listeners[event.type];
for(var i=0;len=listeners.length; i<len;i++){
listeners[i].call(this,event);
}
}
},
removerListener:function(type,listener){
if(this._listeners&&this._listeners[type] instanceof ?Array){
var listeners=this._listeners[type];
for(var i=0,len=listeners.length;i<len;i++){
if(listeners[i]===listener){
listeners.splice(i,1);
break;
}
}
}
}
}
EventTarget 類型那個爲(wèi)任何對象提供基本的事件處理。你可以添加和刪除監(jiān)聽者,也可以在對象上直接觸發(fā)事件。事件監(jiān)聽者被存儲在_listeners屬性中,該屬性僅在addListener()第一次被調(diào)用時創(chuàng)建(這讓混入變得簡單了一點)。你可以像下面這樣使用EventTarget的實例。
var target =new EventTarget();
target.addListener("message",function(event){
console.log("Message is "+event.data)})
target.fire({
type:"message",
data:"Hello world!"});
在javascript對象中支持事件十分喲用。如果你像讓另一個對象也支持事件,你有幾種選擇。首先你可以創(chuàng)建一個新的Event.target實例並添加任何你需要的屬性,如下
var person =new EventTarget();
person.name="Nicholas";
person.sayName=function(){
console.log(this.name);
this.fire({
type:"namesaid",
name:name
})};
在這段代碼中,一個新的變量person作爲(wèi)EventTarget的實例被創(chuàng)建出來,然後添加各種跟person相關(guān)的屬性??上У臅r,者意味著person實際上時一個EventTarget而不是一個Object或其他自定義類型。另外,你還需要承受手動添加一批新屬性的開銷。如果能有一種更加有組織的方法來幹這件事就更好啦。解決這個問題的方法是使用僞類繼承。
function Person(name){
this.name=name;
}
Person.prototype=Object.create(EventTarget.prototype);
Person.prototype.constructor=Person;
Person.prototype.sayName=function(){
console.log(this.name);
this.fire({
type:"namesaid",
name:name
});
var person=new Person("Nicholas");
console.log(person instanceof Person);
console.log(person instanceof EventTarget);
}
在這裏例子中,一個新的Person類型繼承自EventTarget。隨後你可以Person的原型對象上添加你需要的方法。然而,這還沒有做到足夠簡潔,而且你會抱怨這個關(guān)係說不過去:一個Person是一種EventTarget?通過使用混入,可以用最少的代碼將這些屬性複製到原型對象中。
Function Person(name){
this.name=name;
}
mixin(Person.prototype,new EventTarget());
mixin(Person.prototype,{
constructor:Person,
sayName:function(){
console.log(this.name);
this.fire({
type:"namesaid",name:anme
})};
});
var person =new Person("Nicholas");
console.log(person instanceof Person);
console.log(person instanceof EventTarget);
這裏,Person.prototype混入了EventTarget的一個新實例來後期事件行爲(wèi)。然後,Person.prototype又被混入constructor和sayName()來完成原型對象的組裝。由於本例中沒有繼承,Person的實例不再是EventTarget的實例。
當(dāng)然,有時候你可能需要使用一個對象的屬性,但不想要僞類繼承的構(gòu)造函數(shù)。這時候,你可以使用混入來創(chuàng)建自己的對象。
var person =mixin(new EventTarget(),{
name:"Nicholas",
sayName:function(){
console.log(this.name);
this.fire({
type:"namesaid",name:name
})}
});
在這個例子中,一個EventTarget實例混入來一些新的屬性來創(chuàng)建person對象而沒有改變person的原型對象鏈。
以這種方式使用混入時需要記住一件事,提供者的訪問器屬性會編程接收者的數(shù)據(jù)屬性,這意外者你如果不當(dāng)心,有可能改寫它們。這是因爲(wèi)接收者的屬性時被賦值語句而不是Object.defineProperty()創(chuàng)建,提供者的屬性當(dāng)前的值被讀取後賦值給接收者的同名屬性。如下例。
var person =mixin(new EventTarget(),{
get name(){
return "Nicholas";
},
sayName:function(){
console.log(this.name);
this.fire({
type:"namesaid",
name:name
})
}
});
console.log(person.name);
person.name="Greg";
console.log(person.name);
這段代碼定義了僅有g(shù)etter的訪問器屬性name。這意味著對該屬性賦值應(yīng)該不起作用。然而,由於在person對象里該訪問器屬性變成了數(shù)據(jù)屬性,你就有可能改寫name的值。在調(diào)用mixin()時,提供者name屬性的值被讀取後賦值給接收者的name屬性。在這個過程中沒有機會定義一個新的訪問器屬性,從而時接收者的name屬性稱爲(wèi)一個數(shù)據(jù)屬性。
如果你想要訪問器屬性被複製成訪問器屬性,需要一個不同的mixin()函數(shù)。
funciton mixin(receiver,supplier){
Object.keys(supplier).forEach(function(property){
var descriptor=Object.getOwnPropertyDescriptor(supplier,property);
Object.defineProperty(receiver,property,descriptor);
});
return receiver;
}
var perso =mixin(new EventTarget(),{
get name(){
return "Nicholas";
},
sayName:function(){
console.log(this.name);
this.fire({
type:"namesaid",
name:name
})
}
});
console.log(person.name);
person.name="Greg";
console.log(person.name);
這個版本的mixin()使用Object.keys()獲得提供者所有的可枚舉自有屬性。在這組屬性上用forEach()方法迭代,對提供者每一個屬性獲取其屬性描述符,然後通過Object.defineProperty()添加給接收者,確保所有的屬性相關(guān)信息都被傳遞給接收者,而不只是屬性的值。這意味著person對象會有一個訪問器屬性名爲(wèi)name,所有它無法改寫。
當(dāng)然這個版本的mixin()只能工作在Ecmascript5的javascript引擎上,如果你的代碼需要在老版本的引擎上工作,可以將兩種mixin()結(jié)合到一個函數(shù)里。
function mixing(receiver,supplier){
if(Object.getOwnPropertyDescriptor){
Object.keys(supplier).forEach(function(property){
var descriptor=Object.getOwnPropertyDescriptor(supplier,property);
Object.defineProperty(receiver,property,descriptor);
});
}else{
for(var property in supplier){
if(supplier.hasOwnProperty(property)){
receiver[property]=supplier[property]
}
}
}
return receiver;
}
這裏,mixin()通過檢查Object.getOwnPropertyDescriptor()是否存在決定javascript引擎是否支持ecmaScript5。如果支持則使用ecmascript5,否則使用ecmascript3的版本。這個阿訇唸書可同時被新老javascript引擎使用。因爲(wèi)它們會選取最何時的混入策略。
注意:Object.keys()值返回可枚舉手續(xù)ing。如果還想要複製不可枚舉屬性,而可以使用Object.getOwnPropertyNames()來代替
6.3 作用域安全的構(gòu)造函數(shù)
構(gòu)造函數(shù)也是函數(shù),所以可以不用new操作符直接調(diào)用它們來改變this的值。在非嚴格模式下,this被強制指向全局對象,這麼做會導(dǎo)致無法預(yù)知的結(jié)果,而在嚴格模式下,構(gòu)造函數(shù)會拋出一個錯誤。
function Person(name){
this.name=name;
}
Person.prototype.sayName=function(){
console.log(this.name);
};
var person1= Person("Nicholas");
console.log(person1 instanceof Person);
console.log(typeof person1);
console.log(name);
這個例子里,由於Person構(gòu)造函數(shù)不是用new操作符調(diào)用的,我們創(chuàng)建來一個全局變量name。這段代碼運行與非嚴格莫俄式,如果在嚴格模式下這麼做會拋出一個錯誤。首字母大寫的構(gòu)造函數(shù)通常就是在提醒你記得在前面加上new操作符,但是你就是想要這麼用怎麼辦?很多內(nèi)建構(gòu)造函數(shù),例如Array和RegExp不需要new操作符也可以工作,這是因爲(wèi)它們被設(shè)計作爲(wèi)作用域安全的構(gòu)造函數(shù),一個作用域安全的構(gòu)造函數(shù)有沒有new都可以工作,並返回同樣類型的對象。
當(dāng)用new調(diào)用一個函數(shù)時,this指向的新創(chuàng)建的對象已經(jīng)屬於該構(gòu)造函數(shù)所代表的自定義類型。也就是所,可以在函數(shù)內(nèi)用instanceof來檢查自己是否被new調(diào)用。
function Person(name){
if(this instanceof Person){
}else{
? ? ? ?}
}
使用這種模式,你可以根據(jù)new的使用與否來控制函數(shù)的行爲(wèi)??赡苣阆胍诓煌那闆r下都表現(xiàn)出相同的行爲(wèi)(常常爲(wèi)了保護那些偶然忘記使用new的情況)。一個作用域安全的Person的版本如下。
function ?Person(name){
if(this instanceof Person){
? ? this.name=name;
? ? ? ? }else{
? ?return new Person(name);
? ?}
}
對於這個構(gòu)造函數(shù),當(dāng)自己是被new調(diào)用時則設(shè)置name屬性,如果不是被new調(diào)用,則以new遞歸調(diào)用自己來爲(wèi)對象創(chuàng)建正確的實例。這麼做,就能確保下面的行爲(wèi)一致了。
var person=new Person("Nicholas");
var person1= Person("Nicholas");
console.log(person1 instanceof Person);
console.log(person2 instanceof Person);
這種不使用new創(chuàng)建新對的做法已經(jīng)相當(dāng)常見了。javascript本身提供很多作用域安全的函數(shù)。例如Object,Array,RegExp,和Error。
6.4 總結(jié)
javascript有很多不同的方式創(chuàng)建和組裝對象。雖然javascript沒有一個正式的私有屬性的概念,但是你可以創(chuàng)建僅在對象內(nèi)可以訪問的數(shù)據(jù)或函數(shù)。對於單件對象,你可以使用模塊模式對外界隱藏數(shù)據(jù)??梢允褂昧⒄{(diào)函數(shù)表達(IIFE)定義僅被新創(chuàng)建的對象訪問的班底變量和函數(shù)。特權(quán)方法時可以訪問對象私有數(shù)據(jù)的方法。你還可以創(chuàng)建僅僅有私有數(shù)據(jù)的構(gòu)造函數(shù),一種方法時在構(gòu)造函數(shù)內(nèi)定義變量,另一種方法時使用IIFE來創(chuàng)建所有實例共享的私有數(shù)據(jù)。
混入時一種給對象添加功能,同時避免繼承的強有力的方式?;烊雽⒁粋€屬性從一個對象複製到另一個對象,從而使得接收者在不需要繼承提供的情況下獲取其功能。和繼承不同,混入令你在創(chuàng)建對象後無法檢查屬性來源。因此,混入最適合被用於數(shù)據(jù)屬性或小函數(shù)。若你想要獲得更強大的功能且需要知道該功能來自哪裏,繼承仍然是我們推薦的做法。
作用域安全的構(gòu)造函數(shù)時用不用new都可以被調(diào)用來生成新的對象實例的構(gòu)造函數(shù)。這種模式之所以能工作,是因爲(wèi)this在構(gòu)造函數(shù)一開始執(zhí)行時就已經(jīng)指定自定義類型的實例,你可以根據(jù)new的使用與否來決定構(gòu)造函數(shù)的行爲(wèi)。
最後祝大家新春快樂,闔家歡樂?。。。。。。。。。。?016.2.4 00:16