標籤:write 派生 子類 enum pos 問題 software struct tor
ECMA-262把對象定義為:“無序屬性的集合,其屬性可以包含基本值、對象或函數”。
一、理解對象
建立自訂對象的最簡單方式就是建立一個Object的執行個體,然後再為它添加屬性和方法。
1 屬性類型
1)資料屬性
資料屬性包含一個資料值的位置。在這個位置可以讀取和寫入值。資料屬性有4個描述其行為的特性。
- [[Configurable]]:表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。
- [[Enumerable]]:表示能否通過for-in迴圈返回屬性。
- [[Writeable]]:表示能否修改屬性的值。
- [[Value]]:包含這個屬性的資料值。
要修改屬性預設的特性,必須使用ECMAScript 5的Object.defineProperty()方法。這個方法接受三個參數:屬性所在的對象、屬性的名字和一個描述符對象。其中,描述符(descriptor)對象的屬性必須是:configurable、enumerable、writable和value。
2)訪問器屬性
訪問器屬性不包含資料值:它們包含一對兒getter和setter函數。在讀取存取器屬性時,會調用getter函數,這個函數負責返回有效值;在寫入訪問器屬性時,會調用setter函數並傳入新值,這個函數負責決定如何處理資料。
- [[Configurable]]:表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為資料屬性。
- [[Enumerable]]:表示能否通過for-in迴圈返回屬性。對於直接在對象上定義的屬性,這個特性的預設值為true。
- [[Get]]:在讀取屬性時調用的函數。
- [[Set]]:在寫入屬性時調用的函數。
訪問器屬性不能直接定義,必須使用Object.defineProperty()來定義。
不一定非要同時指定getter和setter。只指定getter意味著屬性是不能寫,嘗試寫入屬性會被忽略,在strict 模式下,嘗試寫入只指定了getter函數的屬性會拋出錯誤。類似地,只指定setter函數的屬性也不能讀,否則在非strict 模式下會返回undefined,而在strict 模式下會拋出錯誤。
2 定義多個屬性
由於為對象定義多個屬性的可能性很大,ECMAScript 5又定義了一個Object.defineProperties()方法。利用這個方法可以通過描述符一次定義多個屬性。這個方法接受兩個對象參數:第一個對象是要添加和修改其屬性的對象,第二個對象的屬性與第一個對象中要添加或修改的屬性一一對應。
3 讀取屬性的特性
使用ECMAScript 5的Object.getOwnPropertyDescriptor()方法,可以取得給定屬性的描述符。這個方法接受兩個參數:屬性所在的對象和讀取其描述符的屬性名稱。傳回值是一個對象,如果是訪問器屬性,這個對象的屬性有configurable、enumerable、get和set;如果是資料屬性,這個對象的屬性有configuration、enumerable、writable和value。
二、建立對象
1 原廠模式
function createPerson(name, age, job) { var o = { name: name, age: age, job: job, sayName: function() { alert(this.name); } }; return o;}var person = createPerson(‘Greg‘, 27, ‘Doctor‘);
2 建構函式模式
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function(){ alert(this.name); };}var person = new Person(‘Greg‘, 27, ‘Doctor‘);
與原廠模式不同之處:
- 沒有顯式地建立對象;
- 直接將屬性和方法賦給了this對象;
- 沒有return語句。
以這種方式調用建構函式實際上會經曆以下4個步驟:
- 建立一個新對象;
- 將建構函式的範圍賦給新對象;
- 執行建構函式中的代碼(為這個新對象添加屬性);
- 返回新對象。
1)將建構函式當作函數
建構函式與其他函數的唯一區別,就在於調用它們的方式不同。
//當作建構函式使用var person = new Person(‘Greg‘, 27, ‘Doctor‘);person.sayName();//"Grey"//作為普通函數調用Person("Grey", 27, "Doctor");window.sayName();//"Grey"//在另一個對象的範圍中調用var o = new Object();Person.call(o, "kristen", 25, "Nurse");o.sayName();
2)建構函式的問題
使用建構函式的主要問題,就是每個方法都要在每個執行個體上重新建立一遍。
3 原型模式
每個函數都有一個prototype(原型)屬性,這個屬性是一個指標,指向一個對象,而這個對象的用途是包含可以由特定類型的所有執行個體共用的屬性和方法。使用原型對象的好處是可以讓所有對象執行個體共用它所包含的屬性和方法。
1)理解原型對象
無論什麼時候,只要建立了一個新函數,就會根據一組特定的規則為該函數建立一個prototype屬性,這個屬性指向函數的原型對象。在預設情況下,所有原型對象都會自動獲得一個constructor(建構函式)屬性,這個屬性是一個指向prototype屬性所在函數的指標。
建立了自訂的建構函式以後,其原型對象預設只會取得constructor屬性;至於其他方法,則都是從Object繼承而來的。當調用建構函式建立一個新執行個體後,該執行個體的內部將包含一個指標(內部屬性),指向建構函式的原型對象。ECMA-262第5版中管這個指標叫[[Prototype]]。雖然在指令碼中沒有標準的方式訪問[[Prototype]],但Firefox、Safari和Chrome在每個對象上都支援一個屬性__proto__;而在其他實現中,這個屬性對指令碼則是完全不可見的。
雖然在所有實現中都無法訪問到[[Prototype]],但可以通過isPrototype()方法來確定對象之間是否存在這種關係。
ECMAScript 5增加了一個新方法,叫Object.getPrototypeOf(),在所有支援的實現中,這個方法返回[[Prototype]]的值。
雖然可以通過對象執行個體訪問儲存在原型中的值,但卻不能通過對象執行個體重寫原型中的值。如果我們在樣本中添加了一個屬性,而該屬性會與執行個體原型中的一個屬性同名,那我們就在執行個體中建立該屬性,該屬性將會屏蔽原型中的那個屬性。
使用hasOwnProperty()方法可以檢測一個屬性是存在於樣本中,還是存在在原型中。這個方法(不要忘了它是從Object繼承而來的)只在給定屬性存在於對象樣本中時,才會返回true。
2)原型與in操作符
有兩種方式使用in操作符:單獨使用和在for-in迴圈中使用。在單獨使用時,in操作符會在通過對象能夠訪問給定屬性時返回true,無論該屬性存在於樣本中還是原型中。
在使用for-in迴圈時,返回的是所有能夠通過對象訪問的、可枚舉的(enumerated)屬性,其中即包括存在於執行個體中的屬性,也包括存在於原型中的屬性。屏蔽了原型中不可枚舉屬性(即[[Enumerable]]標記為false的屬性)的執行個體屬性也會在for-in迴圈中返回,因為根據規定,所有開發人員定義的屬性都是可枚舉的。
要取得對象上所有可枚舉的執行個體屬性,可以使用ECMAScript 5的Object.keys()方法。這個方法接受一個對象作為參數,返回一個包含所有可枚舉屬性的字串數組。
如果你想要得到所有執行個體屬性,無論它是否可枚舉,都可以使用Object.getOwnPropertyNames()。
3)更簡單的原型文法
function Person() {}Person.prototype = { name: "Nicholas", age: 29, job: "Software Engineer", sayName: function() { alert(this.name); } }//重設建構函式,只適用於ECMAScript 5相容瀏覽器Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person}
4)原型的動態性(重要)
調用建構函式時會為執行個體添加一個指向最初原型的[[Prototype]]指標,而把原型修改為另一個對象就等於切斷了建構函式於最初原型之間的聯絡。
function Person() {}var friend = new Person();Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", sayName: function() { alert(this.name); }};friend.sayName();
在這個例子中,我們先建立了Person的一個執行個體,然後又重寫了其原型對象。然後在調用friend.sayName()時發生錯誤,因為friend指向的原型中不包含以該名字命名的屬性。
5)原型對象的問題
原型對象的最大問題是由其共用的本性所導致的。
4 組合使用建構函式模式和原型模式
建構函式模式用於定義執行個體屬性,而原型模式用於定義方法和共用屬性。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "Court"];}Person.prototype = { constructor: Person, sayName: function() { alert(this.name); }}
5 動態原型模式
動態原型模式把所有資訊都封裝在了建構函式中。
function Person(name, age, job) { //屬性 this.name = name; this.age = age; this.job = job; //方法 if(typeof this.sayName != "function") { Person.prototype.sayName = function() { alert(this.name); } }}
三、繼承
1 原型鏈
原型鏈是實現繼承的主要方法。其基本思想是利用原型讓一個參考型別繼承另一個參考型別的屬性和方法。
function SuperType() { this.property = true;}SuperType.prototype.getSuperValue = function() { return this.prototype;}function SubType() { this.subprototype = false;}//繼承了SuperTypeSubType.prototype = new SubType();
1)確定原型和執行個體的關係
可以通過兩種方式來確定原型和執行個體之間的關係。第一種方式是使用instanceof操作符,只要用這個操作符來測試執行個體與原型鏈中出現過的建構函式,結果就會返回true。第二種方式是使用isPrototypeOf()方法。同樣,只要是原型鏈中出現過的原型,都可以說是該原型鏈所派生的執行個體的原型,因此isPrototypeOf()方法也會返回true。
2)原型鏈的問題
原型鏈雖然很大,可以用它來實現繼承,但它也存在一些問題。其中,最主要的問題來自包含參考型別值的原型。
2 借用建構函式
在解決原型中包含參考型別值所帶來問題的過程中,開發人員開始使用一個叫做借用建構函式(constructor stealing)的技術(有時候也叫做偽類對象或經典繼承)。這中技術的基本思想相當簡單,即在子類型建構函式的內部調用超類型建構函式。
function SuperType() { this.colors = ["red", "blue", "green"];}function SubType() { SuperType.call(this);}
1)傳遞參數
相對於原型鏈而言,借用建構函式有一個很大的優勢,即可以在子類型建構函式中向超類型建構函式傳遞參數。
2)借用建構函式的問題
如果僅僅是借用建構函式,那麼也將無法避免建構函式模式中存在的問題——方法都在都在建構函式中定義,因此函數複用就無從談起了。
3 組合繼承
組合繼承(combination inheritance),有時候也叫做偽經典繼承,指的是將原型鏈和借用建構函式的技術組合到一塊,從而發揮二者之長的一種繼承模式。其背後的思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用建構函式來實現對執行個體屬性的繼承。
function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"];}SuperType.prototype.sayName = function() { alert(this);}function SubType(name, age) { //繼承屬性 SuperType.call(this, name); this.age = age;}//繼承方法SubType.prototype = new SuperType();SubType.prototype.constructor = SubType;SubType.prototype.sayAge = function() { alert(this.age);};
4 原型式繼承
藉助原型可以基於已有的對象建立新對象,同時還不必因此建立自訂類型。
function object(o) { function F() {} F.prototype = o; return new F();}
在object()函數內部,先建立了一個臨時的建構函式,然後將傳入的對象作為這個建構函式的原型,最後返回這個臨時類型的一個執行個體。從本質上講,object()傳入其中的對象執行了一次淺複製。
[筆記]《JavaScript進階程式設計》- 物件導向的程式設計