標籤:his for 順序 alt 應該 學習 cal 相同 str
歡迎關注本人的公眾號“前端小填填”,專註前端技術的基礎和項目開發的學習。
本節內容對應《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]
- 由於為對象定義多個屬性的可能性很大,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操作符來調用,那它跟普通函數也不會有什麼兩樣。
//當作建構函式使用 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" 建構函式雖然好用,但也並非沒有缺點。使用建構函式的主要問題,就是每個方法都要在每個執行個體上重新建立一遍。在前面的例子中,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; }
原型模式:我們建立的每個函數都有一個prototype(原型)屬性,這個屬性是一個指標,指向一個對象,而這個對象的用途是包含可以由特定類型的所有執行個體共用的屬性和方法。如果按照字面意思來理解,那麼prototype就是通過調用建構函式而建立的那個對象執行個體的原型對象。使用原型對象的好處是可以讓所有對象執行個體共用它所包含的屬性和方法。換句換說,不必在建構函式中定義對象執行個體的資訊,而是可以將這些資訊直接添加到原型對象中。
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 在這個例子中,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(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();
《JavaScript進階程式設計》學習筆記(5)——物件導向編程