《JavaScript高階程式設計》學習筆記(5)——面向物件程式設計
歡迎關注本人的微信公眾號“前端小填填”,專注前端技術的基礎和專案開發的學習。
本節內容對應《JavaScript高階程式設計》的第六章內容。
1、面向物件(Object-Oriented, OO)的語言有一個標誌,那就是它們都有類的概念,而通過類可以建立任意多個具有相同屬性和方法的物件。前面提到過,ECMAScript中沒有類的概念,因此它的物件也與基於類的語言中的物件有所不同。
ECMA-262把物件定義為:“無序屬性的集合,其屬性可以包含基本值、物件或者函式。”嚴格來講,這就相當於說物件是一組沒有特定順序的值。物件的每個屬性或方法都有一個名字,而每個名字都對映到一個值。正因為這樣(以及其他將要討論的原因),我們可以把ECMAScript的物件想象成散列表:無非就是一組名值對,其中值可以是資料或函式
每個物件都是基於一個引用型別建立的,這個引用型別可以是前面討論的原生型別,也可以是開發人員定義的型別。
2、屬性型別:ECMA-262第5版在定義只有內部才用的特性時,描述了屬性的各種特徵。ECMA-262定義這些特性是為了實現JavaScript引擎用的,因此在JavaScript中不能直接訪問它們。為了表示特性是內部值,該規範把它們放在了兩對方括號中,例如[[Enumerable]]。
-
資料屬性:包含一個數據值的位置。在這個位置可以讀取和寫入值。資料屬性有四個描述其行為的特性。
- [[Configurable]]:表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。像前面例子中那樣直接在物件上定義的屬性,它們的這個特性預設值為true
- [[Enumerable]]:表示能否通過for-in迴圈返回屬性。像前面例子中那樣直接在物件上定義的屬性,它們的這個特性預設值為true。
- [[Writable]]:表示能否修改屬性的值。像前面例子中那樣直接在物件上定義的屬性,它們的這個特性預設值為true。
- [[Value]]:包含這個屬性的資料值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值儲存在這個位置。這個特性的預設值為undefined。
要修改屬性預設的特性,必須使用ECMAScript5的Object.defineProperty()方法。這個方法接收三個引數:屬性所在的物件、屬性的名字和一個描述符物件。其中,描述符物件的屬性必須是:configurable、enumerable、writable和value。設定其中的一或多個值,可以修改對應的特性
var person = {
name:"Nicholas",
age:29,
toString:function(){
return "[name=" + this.name + "; age=" + this.age + "]";
}
};
Object.defineProperty(person , "name",{
writable:false
});
person.name="goskalrie";//修改無效
alert(person);//[name=Nicholas; age=29]
- 訪問器屬性:不包含資料值;它們包含一對getter和setter函式(不過,這兩個函式都不是必需的)。在讀取訪問器屬性時,會呼叫getter函式,這個函式負責返回有效的值;在寫入訪問器屬性時,會呼叫setter函式並傳入新值。這個函式負責決定如何處理資料。訪問器屬性有如下4個特性。訪問器屬性不能直接定義,必須使用Object.defineProperty()來定義。
- [[Configurable]]:表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為資料屬性。對於直接在物件上定義的屬性,它們的這個特性預設值為true
- [[Enumerable]]:表示能否通過for-in迴圈返回屬性。對於直接在物件上定義的屬性,它們的這個特性預設值為true。
- [[Get]]:在讀取屬性時呼叫的函式。預設值為undefined。
- [[Set]]:在寫入屬性時呼叫的函式。預設值為undefined。
var book = {
_year:2004,
edition:1
};
Object.defineProperty(book , "year" , {
get:function(){
return this._year;
},
set:function(newValue){
if(newValue > 2004){
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
book.year = 2005;
alert(book.edition);//2
- 由於為物件定義多個屬性的可能性很大,ECMAScript5又定義了一個Object.defineProperties()方法。利用這個方法可以通過描述一次定義多個屬性。這個方法接收兩個物件引數:第一個物件是要新增和修改其屬性的物件,第二個物件的屬性與第一個物件中要新增或修改的屬性一一對應。以下程式碼在book對外上定義了兩個資料屬性(_year和edition)和一個訪問器屬性(year)。
var book = {};
Object.defineProperties(book , {
_year:{
value:2004
},
edition:{
value:1
},
year:{
get:function(){
return this._year;
},
set:function(newValue){
if(newValue > 2004){
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
- 讀取屬性的特性:使用ECMAScript5的Object.getOwnPropertyDescriptor()方法,可以取得給定屬性的描述符。這個方法接收兩個引數:屬性所在的物件和要讀取其描述符的屬性名稱。返回值是一個物件,如果是訪問器屬性,這個物件的屬性有configurable、enumerable、get和set;如果是資料屬性,這個對像的屬性有configurable、enumerable、writable和value。
var descriptor =Object.getOwnPropertyDescriptor(book , "_year");
alert(descriptor.value);//2004
alert(descriptor.configurable);//false
alert(typeof descriptor.get);//"undefined"
3、建立物件:雖然Object建構函式或物件字面量都可以用來建立單個物件,但這些方式有個明顯的缺點:使用同一個介面建立很多物件,會產生大量的重複程式碼。下面介紹多找種建立物件的方法:
- 工廠模式:工廠模式是軟體工程領域一種廣為人知的設計模式,這種模式抽象了建立具體物件的過程。考慮到在ECMAScript中無法建立類,開發人員就發明了一種函式,用函式來封裝以特定介面建立物件的細節。工廠模式雖然解決了建立多個相似物件的問題,但卻沒有解決物件識別的問題(即怎樣知道一個物件的型別)。
function createPerson(name , age){
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sayName = function(){
alert(this.name);
};
return obj;
}
var person1 = createPerson("Nicholas" , 29);
var person2 = createPerson("Greg" , 21);
- 建構函式模式:前面介紹過,ECMAScript中的建構函式可用來建立特定型別的物件。像Object和Array這樣的原生建構函式,在執行時會自動出現在執行環境中。此外,也可以建立自定義的建構函式,從而定義自定義物件型別的屬性和方法。利用建構函式建立例項,必須使用new操作符。注意和工廠模式進行比較其不同點。
function Person(name , age){
this.name = name;
this.age = age;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Nicholas" , 29);
var person2 = new Person("Greg" , 21);
- 建構函式與其他函式的唯一區別,就在於呼叫它們的方式不同。不過,建構函式畢竟也是函式,不存在定義建構函式的特殊語法。任何函式,只要通過new操作符來呼叫,那它就可以作為建構函式;而任何函式,如果不通過new操作符來呼叫,那它跟普通函式也不會有什麼兩樣。 建構函式雖然好用,但也並非沒有缺點。使用建構函式的主要問題,就是每個方法都要在每個例項上重新建立一遍。在前面的例子中,person1和person2都有一個名為sayName()的方法,但那兩個方法不是同一個Function的例項。不要忘了——ECMAScript中的函式是物件,因此每定義一個函式,也就是例項化了一個物件。從這個角度上來看建構函式,更容易明白每個Person例項都包含一個不同的Function例項的本質。說明白些,以這種方式建立函式,會導致不同的作用域鏈和識別符號解析,但建立Function新例項的機制仍然是相同的。因此,不同例項上的同名函式是不相等的,以下程式碼可以證明這一點 alert(person.sayName ==person2.sayName);//false ,建立兩個完成同樣任務的Function例項的確沒有必要;況且this物件在,根本不用在執行程式碼前就把函式繫結到特定物件上面。因此,通過把函式定義到建構函式外部來解決這個問題,這樣做確實解決了兩個函式做同一件事的問題,可是新問題又來了:在全域性作用域中定義的函式實際上只能對某個物件呼叫,這讓全域性作用域有點名不副實。而更讓人無法接受的是:如果物件需要定義很多方法,那麼就要定義很多個全域性函式,於是這個自定義的引用型別就絲毫沒有封裝性可言了。好在,這些問題可以通過使用原型模式來解決。 function sayName(){ alert(this.name); } function Person(name , age){ this.name = name; this.age = age; this.sayName = sayName; }
//當作建構函式使用
var person = new Person("Nicholas" , 29);
person.sayName();//"Nicholas"
//作為普通函式呼叫
Person("Greg" , 21);//新增到window
window.sayName();//"Greg"
var o = new Object();
Person.call(o , "goser" , 24);
o.sayName();//"goser"
- 原型模式:我們建立的每個函式都有一個prototype(原型)屬性,這個屬性是一個指標,指向一個物件,而這個物件的用途是包含可以由特定型別的所有例項共享的屬性和方法。如果按照字面意思來理解,那麼prototype就是通過呼叫建構函式而建立的那個物件例項的原型物件。使用原型物件的好處是可以讓所有物件例項共享它所包含的屬性和方法。換句換說,不必在建構函式中定義物件例項的資訊,而是可以將這些資訊直接新增到原型物件中。 在這個例子中,person1的name被一個新值給遮蔽了。但無論訪問person1.name還是訪問person2.name都能正常地返回值,即分別是”Greg”和”Nicholas”。當在alert()中訪問person1.name時,需要讀取它的值,因此就會在這個例項上搜索一個名為name的屬性,這個屬性確實存在,於是就返回它的值,不必再搜尋原型了。當以相同的方式訪問person2.name時,並沒有在例項上發現該屬性,因此就會繼續搜尋原型,結果在那裡找到了name屬性。使用delete操作符刪除了person1.name,之前它儲存的”Greg”值遮蔽了同名的原型屬性。把它刪除以後,就恢復了對原型中name屬性的連線。因此,接下來再呼叫person1.name時,返回的就是原型中name屬性的值了。 //更簡單的原型模式的寫法 function Person(){} Person.prototype = { name : "Nicholas", age : 29, sayName : function(){alert(this.name);} };
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.sayName = function(){alert(this.name);};
var person1 = new Person();
person1.sayName();//"Nicholas"
var person2 = new Person();
person2.sayName();//"Nicholas"
person1.name = "Greg";
alert(person1.name);//"Greg"——來自例項
delete person1.name;
alert(person1.name);//"Nicholas"——來自原型
alert(person1.sayName == person2.sayName);//true
- 組合使用建構函式模式和原型模式:建立自定義型別的最常見方式,就是組合使用建構函式模式與原型模式。建構函式模式用於定義例項屬性,而原型模式用於定義方法和共享的屬性。結果,每個例項都會有自己的一份例項屬性的副本,但同時又共享著對方法的引用,最大限度地節省了記憶體。另外,這種混成模式還支援向建構函式傳遞引數;可謂是集兩種模式之長
function Person(name , age){
this.name = name;
this.age = age;
this.friends = ["goser" , "greg"];
}
Person.prototype = {
constructor : Person,
sayName : function(){alert(this.name);},
};
var person1 = new Person("Nicholas" , 29);
var person2 = new Person("Greg" , 21);
person1.friends.push("gat");
alert(person1.friends);//"goser,greg,gat"
alert(person2.friends);//"goser,greg"
alert(person1.friends == person2.friends);//false
alert(person1.sayName == person2.sayName);//true
- 動態原型模式:有其他OO語言經驗的開發人員在看到獨立的建構函式和原型時,很可能會感到非常困惑。動態原型模式正式致力於解決這個問題的一個方案。它把所有資訊都封裝在了建構函式中,而通過在建構函式中初始化原型(僅在必要的情況下),又保持了同時使用建構函式和原型的優點。換句話說,可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。
function Person(name , age){
this.name = name;
this.age = age;
-----------------------------------------//
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
-----------------------------------------//
}
var p = new Person("Nicholas" , 29);
p.sayName();
- 寄生建構函式模式:通常,在前述的幾種模式都不適用的情況下,可以使用寄生建構函式模式。這種模式的基本思想是建立一個函式,該函式的作用僅僅是封裝建立物件的程式碼,然後再返回新建立的物件;但從表面上看,這個函式又很像是經典的建構函式。實際上就是設計模式 中的組合模式的應用吧。
function Person(name , age){
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sayName = function(){
alert(this.name);
};
return obj;
}
var p = new Person("Nicholas" , 29);
p.sayName();