第六章 對象模式
javascript有很多創建對象的模式,完成工作的方式也不只一種。你可以隨時定義自己的類型或自己的泛用對象。可以使用繼承或混入等其他技術令對象間行爲共享。也可以利用javascript高級技巧來阻止對象結構被改變。本章討論的模式賜予你強大的管理和創建對象的能力,完全基於你自己的用例。
6.1 私有成員和特權成員
javascript對象的多有屬性都是公有的,且沒有顯式的方法指定某個屬性不嗯你被外界某個對象訪問。然而,有時你可能不惜我數據公有。例如,當一個對象使用一個值來決定某種狀態,在對象不知情的情況下修改該值會讓狀態管理變得混亂。一種避免它的方法時通過使用命名規則。例如,在不希望公有的屬性名字加上下劃線(如this._name)。還有很多其他方法不需要依賴命名規則,因此在阻止私有信息被修改方面也就更加“防彈”。
6.6.1 模塊模式
模塊模式是一種用於創建擁有私有數據的單件對象的模式。基本做法時時喲還能夠里調函數表達(IIFE)來返回一個對象。IIFE是一種被定義後立即調用並產生結果的函數表達,該函數表達可以包括任意數量的本地變量,它們在函數外不可見。因爲返回的對象被定義在函數內部,對象的方法可以訪問這些數據。(IIFE定義的所有對象都可以訪問同樣的本地變量)以這種方式訪問私有數據的方式被稱爲特權方法。下面時模塊模式的基本格式。
var yourObject=(function(){
//private data variable
return{
//public methods and properties
}
}())
該模式創建來一個匿名函數並立即執行。注意在函數尾部有額外的小括號,你可以用這種語法立刻執行匿名函數。這意味着這個函數僅存在於被調用的瞬間,一旦執行後立即就被銷毀。IIFE是javascript一種非常流行的模式,部分原因就是它在模塊模式中的應用。
模塊模式允許你使用普通變量作爲非公有對象屬性。通過創建閉包函數作爲對象方法來操作它們。閉包函數就是一個可以訪問其作用於外部數據的普通函數。舉例來說,當你在一個函數中訪問一個全局對象,比如網頁瀏覽器中的window,該函數就是在訪問其作用域外的變量。區別是,在模塊模式中,變量定義在IIFE中,而訪問變量的函數也定義在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());
這段代碼使用模塊模式創建了person對象。變量age就是該對象的一個私有屬性。它無法被外界直接訪問,但可以通過對象方法來操作。該對象上有兩個特權方法:getAge()讀取變量age的值,growOlder()讓age自增。這兩個方法都可以直接訪問age,因爲它們都定義在一個IIFE裏面。
模塊模式還有一個變種叫暴露模塊模式,它將所有的變量和方法都組織在IIFE頂部,然後將它們設置到需要被返回的對象上。你可以用暴露模塊模式改寫前例,如下。
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()函數都被設置到返回的對象中,有效地對外界暴露來它們。這段代碼和使用傳統模塊模式的前例一模一樣;然而,有人更喜歡這種模式,因爲它們保證所有的變量和函數聲明都在一處。
6.1.2 構造函數的私有成員
模塊模式在定義單個對象的私有屬性上十分有效,但對於那些同樣需要私有屬性的自定義類型又如何呢?你可以在構造函數中使用類似的模式來創建每個實例的私有數據。
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構造函數有一個本地變量age。該變量被用於getAge()和growOlder()方法。當你創建Person的一個實例時。該實例接受其自身的age變量。getAge()方法和growOlder()方法。這種做法在很多方面都類似模塊模式,構造函數創建一個本地作用域並返回this對象。在第四章討論過,將方法直接放在對象的實例上不如放在其原型對象上有效,但如果你絮語奧實例私的數據,這是唯一可行的手段。
如果你需要所有實例可共享私有數據,可以結合模塊模式和構造函數。如下
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構造函數被定義在一個IIFE中。變量age被定義在構造函數外並被兩個原型對象的方法使用。IIFE返回InnerPerson構造函數作爲全局作用域里的Person構造函數。最終,Person的全部實例得以共享age變量,所以在一個實例上的改變自動影響了另一個。
6.2 混入
Javascript中大量使用來味蕾繼承和原型對象繼承,還有另一種僞繼承的手段叫混入。一個對象在不改變原型對象鏈的情況下得到來另一個對象屬性的手段叫混入。第一個對象(接受者)同構直接複製第二個對象(提供者)的屬性從而接收 了這些屬性。下面是傳統的利用函數實現的混入。
function mixing(receiver,supplier){
for(var property in supplier){
if(supplier.hasOwnProperty(property)){
receiver[property]=supplier[property]}
}}
return receiver;
函數mixin()接受兩個參數:接受者和提供者。該函數的目的將提供者所有的可枚舉的屬性賦值給接受者。可以通過使用for-in循環迭代提供者的屬性並將值設置給接受者的同名屬性達成這一目的。記住這是淺拷貝,所有如果屬性內包含的時一個對象,那麼提供者和接受者將指向同一個對象。這個模式被廣泛用於將一個javascript對象內已經存在的行爲添加到另一個對象中去。
假如,可以通過混入而不是繼承給一個對象添加事件支持。首先,假設你已經有一個支持事件的自定義類型。
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 類型那個爲任何對象提供基本的事件處理。你可以添加和刪除監聽者,也可以在對象上直接觸發事件。事件監聽者被存儲在_listeners屬性中,該屬性僅在addListener()第一次被調用時創建(這讓混入變得簡單了一點)。你可以像下面這樣使用EventTarget的實例。
var target =new EventTarget();
target.addListener("message",function(event){
console.log("Message is "+event.data)})
target.fire({
type:"message",
data:"Hello world!"});
在javascript對象中支持事件十分喲用。如果你像讓另一個對象也支持事件,你有幾種選擇。首先你可以創建一個新的Event.target實例並添加任何你需要的屬性,如下
var person =new EventTarget();
person.name="Nicholas";
person.sayName=function(){
console.log(this.name);
this.fire({
type:"namesaid",
name:name
})};
在這段代碼中,一個新的變量person作爲EventTarget的實例被創建出來,然後添加各種跟person相關的屬性。可惜的時,者意味着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的原型對象上添加你需要的方法。然而,這還沒有做到足夠簡潔,而且你會抱怨這個關係說不過去:一個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的一個新實例來後期事件行爲。然後,Person.prototype又被混入constructor和sayName()來完成原型對象的組裝。由於本例中沒有繼承,Person的實例不再是EventTarget的實例。
當然,有時候你可能需要使用一個對象的屬性,但不想要僞類繼承的構造函數。這時候,你可以使用混入來創建自己的對象。
var person =mixin(new EventTarget(),{
name:"Nicholas",
sayName:function(){
console.log(this.name);
this.fire({
type:"namesaid",name:name
})}
});
在這個例子中,一個EventTarget實例混入來一些新的屬性來創建person對象而沒有改變person的原型對象鏈。
以這種方式使用混入時需要記住一件事,提供者的訪問器屬性會編程接收者的數據屬性,這意外者你如果不當心,有可能改寫它們。這是因爲接收者的屬性時被賦值語句而不是Object.defineProperty()創建,提供者的屬性當前的值被讀取後賦值給接收者的同名屬性。如下例。
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);
這段代碼定義了僅有getter的訪問器屬性name。這意味着對該屬性賦值應該不起作用。然而,由於在person對象里該訪問器屬性變成了數據屬性,你就有可能改寫name的值。在調用mixin()時,提供者name屬性的值被讀取後賦值給接收者的name屬性。在這個過程中沒有機會定義一個新的訪問器屬性,從而時接收者的name屬性稱爲一個數據屬性。
如果你想要訪問器屬性被複製成訪問器屬性,需要一個不同的mixin()函數。
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()添加給接收者,確保所有的屬性相關信息都被傳遞給接收者,而不只是屬性的值。這意味着person對象會有一個訪問器屬性名爲name,所有它無法改寫。
當然這個版本的mixin()只能工作在Ecmascript5的javascript引擎上,如果你的代碼需要在老版本的引擎上工作,可以將兩種mixin()結合到一個函數里。
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引擎使用。因爲它們會選取最何時的混入策略。
注意:Object.keys()值返回可枚舉手續ing。如果還想要複製不可枚舉屬性,而可以使用Object.getOwnPropertyNames()來代替
6.3 作用域安全的構造函數
構造函數也是函數,所以可以不用new操作符直接調用它們來改變this的值。在非嚴格模式下,this被強制指向全局對象,這麼做會導致無法預知的結果,而在嚴格模式下,構造函數會拋出一個錯誤。
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構造函數不是用new操作符調用的,我們創建來一個全局變量name。這段代碼運行與非嚴格莫俄式,如果在嚴格模式下這麼做會拋出一個錯誤。首字母大寫的構造函數通常就是在提醒你記得在前面加上new操作符,但是你就是想要這麼用怎麼辦?很多內建構造函數,例如Array和RegExp不需要new操作符也可以工作,這是因爲它們被設計作爲作用域安全的構造函數,一個作用域安全的構造函數有沒有new都可以工作,並返回同樣類型的對象。
當用new調用一個函數時,this指向的新創建的對象已經屬於該構造函數所代表的自定義類型。也就是所,可以在函數內用instanceof來檢查自己是否被new調用。
function Person(name){
if(this instanceof Person){
}else{
}
}
使用這種模式,你可以根據new的使用與否來控制函數的行爲。可能你想要在不同的情況下都表現出相同的行爲(常常爲了保護那些偶然忘記使用new的情況)。一個作用域安全的Person的版本如下。
function Person(name){
if(this instanceof Person){
this.name=name;
}else{
return new Person(name);
}
}
對於這個構造函數,當自己是被new調用時則設置name屬性,如果不是被new調用,則以new遞歸調用自己來爲對象創建正確的實例。這麼做,就能確保下面的行爲一致了。
var person=new Person("Nicholas");
var person1= Person("Nicholas");
console.log(person1 instanceof Person);
console.log(person2 instanceof Person);
這種不使用new創建新對的做法已經相當常見了。javascript本身提供很多作用域安全的函數。例如Object,Array,RegExp,和Error。
6.4 總結
javascript有很多不同的方式創建和組裝對象。雖然javascript沒有一個正式的私有屬性的概念,但是你可以創建僅在對象內可以訪問的數據或函數。對於單件對象,你可以使用模塊模式對外界隱藏數據。可以使用立調函數表達(IIFE)定義僅被新創建的對象訪問的班底變量和函數。特權方法時可以訪問對象私有數據的方法。你還可以創建僅僅有私有數據的構造函數,一種方法時在構造函數內定義變量,另一種方法時使用IIFE來創建所有實例共享的私有數據。
混入時一種給對象添加功能,同時避免繼承的強有力的方式。混入將一個屬性從一個對象複製到另一個對象,從而使得接收者在不需要繼承提供的情況下獲取其功能。和繼承不同,混入令你在創建對象後無法檢查屬性來源。因此,混入最適合被用於數據屬性或小函數。若你想要獲得更強大的功能且需要知道該功能來自哪裏,繼承仍然是我們推薦的做法。
作用域安全的構造函數時用不用new都可以被調用來生成新的對象實例的構造函數。這種模式之所以能工作,是因爲this在構造函數一開始執行時就已經指定自定義類型的實例,你可以根據new的使用與否來決定構造函數的行爲。
最後祝大家新春快樂,闔家歡樂!!!!!!!!!!!2016.2.4 00:16