標籤:開發 刪除 基本 聯絡 例子 語言 針對 搜尋 desc
物件導向的語言有一個標誌,那就是它們都有類的概念,而通過類可以建立任意多個具有相同屬性和方法的對象。
每個對象都是基於一個參考型別建立的,這個參考型別可以是之前的原生類型,也可以是開發人員定義的類型。
1、理解對象
①屬性類型:ECMAScript中有兩種屬性:資料屬性和訪問器屬性。
(1)資料屬性:資料屬性包含一個資料值的位置。在這個位置可以讀取和寫入值,資料戶型有4個描述其行為的特性:
資料屬性有4個描述其行為的特性。
[[Configurable]]:表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。預設值為true
[[Enumerable]]:表示能否通過for-in迴圈返回屬性。預設值為true
[[Writable]]:表示能否修改屬性的值。預設值為true
[[Value]]:包含這個屬性的資料值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值儲存在這個位置,預設為undefined
要修改屬性預設的特性,要使用ECMAScript5的Object.defineProperty()方法。這個方法要接收三個參數:屬性所在的對象、屬性的名字和一個描述符對象。其中,描述符(descriptor)對象的屬性必須是:configurable、enumerable、writable和value。設定其中的一個或多個值,可以修改對應的特性值。
(2)訪問器屬性:訪問器屬性不包含資料值;它們包含一對兒getter和setter函數。在讀取存取器屬性時,會調用getter函數,這個函數負責返回有效值;在寫入訪問器屬性時,會調用setter函數並傳入新值,這個函數負責決定如何處理資料。訪問器屬性有如下4個特性:
[[Configurable]]:表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。預設值為true
[[Enumerable]]:表示能否通過for-in迴圈返回屬性。預設值為true
[[Get]]:在讀取屬性時調用的函數,預設值為undefined
[[Set]]:在寫入屬性時調用的函數,預設值為undefined
訪問器屬性不能直接定義,必須使用Object.defineProperty()來定義
②定義多個屬性
由於為對象定義多個屬性的可能性很大,ECMAScript5定義了一個Object.defineProperties()方法。利用這個方法可以通過描述符一次定義多個屬性。這個方法接收兩個對象參數:第一個對象是要添加和修改其屬性的對象,第二個對象的屬性與第一個對象中要添加或修改的屬性一 一對應。
③讀取屬性的特性
ECMAScript5的Object.getOwnPropertyDescriptor()方法,可以取得給定屬性的描述符。這個方法接收兩個參數:屬性所在的對象和要讀取其描述符的屬性名稱。傳回值是一個對象,可以是訪問器屬性,也可以是資料屬性。
在JavaScript中,可以針對任何對象——包括DOM和BOM對象,使用Object.getOwnPropertyDescriptor()方法。
2、建立對象
雖然Object建構函式或對象字面量都可以用來建立單個對象,但這些方式有個明顯的缺點:使用同一個介面建立很多個物件,會產生大量的重複代碼。為解決這個問題,人們開始使用原廠模式的一種變體。
①原廠模式是軟體工程領域一種廣為人知的設計模式,這種模式抽象了建立具體對象的過程。考慮到ECMAScript中無法建立類,開發人員就發明了一種函數,用函數來封裝以特定介面建立對象的細節
此函數能夠接受的參數來構建一個包含所有必要資訊的person對象。可以數次地調用這個函數,而每次它都會返回一個包含三個屬性一個方法的對象。原廠模式雖然解決了建立多個相似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。隨著JavaScript的發展,一個新的模式又出現了。
②建構函式模式:
與原廠模式的區別:(1)沒有顯示地建立對象(2)直接將屬性和方法賦給了this對象(3)沒有return語句。
按照慣例,建構函式始終都應該以一個大寫字母開頭,而非建構函式則應該以一個小寫字母開頭。
這個例子中,person1和person2分別儲存著Person的一個不同的執行個體。這兩個對象都有一個constructor(建構函式)屬性,該屬性指向Person:
對象的constructor屬性最初是用來標識物件類型的。
建立自訂的建構函式意味著將來可以將它的執行個體標識為一種特定的類型,而這正是建構函式模式勝過原廠模式的地方。在這個例子中,person1和person2之所以同時是Object的執行個體,是因為所有對象均繼承自Object。
(1)將建構函式當做函數:建構函式與其他函數的唯一區別,就在於調用它們的方式不同。不過,建構函式畢竟也是函數,不存在定義建構函式的特殊文法。任何函數,只要通過new操作符來調用,那它就可以作為建構函式;而任何函數,如果不通過new操作符來調用,那它跟普通函數也不會有什麼兩樣。
(2)建構函式的問題:每個方法都要在每個執行個體上重新建立一遍。可以通過把函數定義轉移到建構函式外部來解決這個問題
新問題就是在全域範圍中定義的函數實際上只能被某個對象調用。更無法接受的是如果對象需要定義很多方法,那麼就要定義很多個全域函數。原型模式可以解決。
③原型模式:
我們建立的每個函數都有一個prototype(原型)屬性,這個屬性是一個指標,指向一個對象,而這個對象的用途是包含可以由特定類型的所有執行個體共用的屬性和方法。
prototype就是通過調用建構函式而建立的那個對象執行個體的原型對象。使用原型對象的好處是可以讓所有對象執行個體共用它所包含的屬性和方法。
不必在建構函式中定義對象執行個體的資訊,而是可以將這些資訊直接添加到原型對象中。
(1)理解原型對象:
多個對象執行個體共用原型所儲存的屬性和方法的基本原理:當代碼讀取某個對象的某個屬性時,都會執行一次搜尋,目標是具有給定名字的屬性。搜尋首先從對象執行個體本身開始。如果在執行個體中找到具有給定名字的屬性,則返回該屬性的值。如果沒有找到,則繼續搜尋指標指向的原型對象,在原型對象中尋找具有給定名字的屬性。如果找到,則返回該屬性的值。
原型最初只包含constructor屬性,而該屬性也是共用的,因此可以通過對象執行個體訪問
當為對象執行個體添加一個屬性時,這個屬性就會屏蔽原型對象中儲存的同名屬性。即添加這個屬性會阻止我們訪問原型中的那個屬性,但不會修改那個屬性。即使將這個屬性設定為null,也只會在執行個體中設定這個屬性,而不會恢複其指向原型的串連,不過,使用delete操作符則完全可以刪除執行個體屬性,從而讓我們能夠重新訪問原型中的屬性。
使用hasOwnProperty()方法可以檢測一個屬性是存在執行個體中,還是存在原型中。這個方法(不要忘了它是從Object繼承來的)只在給定屬性存在於對象執行個體時,才會返回true
只有當person1重寫name屬性後才會返回true,因為只有這個時候name才是一個執行個體屬性,而非原型屬性。
(2)原型與in操作符
有兩種方式使用in操作符:單獨使用和在for-in迴圈中使用。
單獨使用:in操作符會在通過對象能夠訪問給定屬性時返回true,無論該屬性存在於執行個體中還是原型中
同時使用hasOwnProperty()方法和in操作符,就可以確定該屬性到底是存在於對象中,還是存在於原型中
由於in操作符只要通過對象能夠訪問到屬性就返回true,hasOwnProperty()只在屬性存在於執行個體中返回true,因此只要in操作符返回true,而hasOwnProperty()返回false,就可以確定屬性是原型中的屬性
name屬性先存在於原型中,因此hasPrototypeProperty()返回true。當在執行個體中重寫name屬性後,該屬性就存在於執行個體中了,因此hasPrototypeProperty()返回false。
在使用for-in迴圈時,返回的是所有能夠通過對象訪問的、可枚舉的(enumerated)屬性,其中既包括存在於執行個體中的屬性,也包括存在於原型中的屬性。屏蔽了原型中不可枚舉屬性(即將[[Enumerable]]標記為false的屬性)的執行個體屬性也會在for-in迴圈返回。
要取得對象上的所有可枚舉的執行個體屬性,可以使用ECMAScript5的object.keys()方法,這個方法接收一個對象作為參數,返回一個包含所有可枚舉屬性的字串數組。
如果是通過Person執行個體調用,則Object.keys()返回的數組只包含“name”和“age”兩個執行個體屬性。
如果你想要得到所有執行個體屬性,無論它是否可枚舉,都可以使用Object.getOwnPropertyNames()方法
注意結果中包含了不可枚舉的constructor屬性。Object.keys()方法和Object.getOwnPropertyNames()方法都可以用來代替for-in迴圈。
(3)更簡單的原型文法
用一個包含所有屬性和方法的對象字面量來重寫整個原型對象
我們將Person.prototype設定為等於一個以對象字面量形式建立的新對象,最終結果相同,但是有一個例外:construction屬性不再指向Person了。constructor屬性變成了新對象的constructor屬性(指向Object建構函式),不再指向Person函數。
(4)原型的動態性。
(5)原生對象的原型:
原型模式的重要性不僅體現在建立自訂類型方面,就連所有原生的參考型別,都是採用這種模式建立的。所有原生參考型別(Object、Array、String等等)都在其建構函式的原型上定義了方法。
④組合使用建構函式模式和原型模式
建立自訂類型的最常見方式,就是組合使用建構函式模式和原型模式。建構函式模式用於定義執行個體屬性,而原型模式用於定義方法和共用的屬性。結果,每個執行個體都會有自己的一份執行個體屬性的副本,但同時又共用著對方法的引用,最大限度地節省了記憶體。這種混成模式還支援向建構函式傳遞參數。
這是用來定義參考型別的一種預設模式
⑤動態原型模式
這段代碼只會在初次調用建構函式時才會執行,此後,原型已經完成初始化,不需要再做什麼修改了。這裡對原型所做的修改,能夠立即在所有執行個體中得到反映。
使用動態原型模式時,不能使用對象字面量重寫原型。如果在已經建立了執行個體的情況下重寫原型,那麼就會切斷現有執行個體與新原型之間的聯絡。
⑥寄生建構函式模式
在前述幾種模式都不適用的情況下,可以使用寄生(parasitic)建構函式模式。這種模式基本思想是建立一個函數,該函數的作用僅僅是封裝建立對象的代碼,然後再返回新建立的對象,但從表面上看,這個函數又很像是典型的建構函式。
3、繼承:ECMAScript只支援實現繼承,而且其實現繼承主要是依靠原型鏈來實現的。
①原型鏈:ECMAScript中描述了原型鏈的概念,並將原型鏈作為實現繼承的主要方法。基本思想是利用原型讓一個參考型別繼承另一個參考型別的屬性和方法。
子類型有時候需要覆蓋超類型中的某個方法,或者需要添加超類型中不存在的某個方法。給原型添加方法的代碼一定要放在替換原型的語句之後。
原型鏈的問題:(1)包含參考型別值的原型。在通過原型來實現繼承時,原型實際上回變成另一個類型的執行個體。(2)在建立子類型的執行個體時,不能向超類型的建構函式中傳遞參數。實踐中很少會單獨使用原型鏈。
②借用建構函式:在解決原型中包含參考型別值所帶來問題的過程中,開發人員開始使用一種叫做借用建構函式的技術。基本思想就是子類型建構函式的內部調用超類型建構函式。
③組合繼承:也叫做偽經典繼承,指的是將原型鏈和借用建構函式的技術組合到一塊,從而發揮二者之長的一種繼承模式。思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用建構函式來實現對執行個體屬性的繼承。這樣,既通過在原型上定義方法實現了函數複用,又能保證每個執行個體都有自己的屬性。
④原型式繼承:
在object()函數內部,先建立一個臨時性的建構函式,然後將傳入的對象作為這個建構函式的原型,最後返回了這個臨時類型的一個新執行個體。
ECMAScript5通過新增Object.create()方法正常化了原型式繼承。這個方法接收兩個參數:一個是用作新對象原型的對象和(可選的)一個為新對象定義額外屬性的對象。在傳入一個參數的情況下,Object.create()與object()方法的行為相同。
⑤寄生式繼承
寄生式繼承時與原型式繼承緊密相關的一種思路。寄生式繼承的思路與寄生建構函式和原廠模式類似,即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最後再像真的是它做了所有工作一樣返回對象。
⑥寄生組合式繼承:組合繼承無論是在什麼情況下,都會調用兩次超類型建構函式,一次是在建立子類型原型的時候,另一次實在子類型建構函式內部。
子類型最終都會包含超類型對象的全部執行個體屬性。
寄生組合式繼承,即通過借用建構函式來繼承屬性,通過原型鏈的混成形式來繼承方法。思路就是:不必為了指定子類型的原型而調用超類型的建構函式,我們需要的無非就是超類型原型的一個副本而已。
Javascript物件導向的程式設計