1. 程式人生 > >【學習筆記】六:面向物件的程式設計——理解JS中的物件屬性、建立物件、JS中的繼承

【學習筆記】六:面向物件的程式設計——理解JS中的物件屬性、建立物件、JS中的繼承

  ES中沒有類的概念,這也使其物件和其他語言中的物件有所不同,ES中定義物件為:“無序屬性的集合,其屬性包含基本值、物件或者函式”。現在常用的建立單個物件的方法為物件字面量形式。在常見多個物件時,使用工程模式的一種變體。

1.理解物件

  1)物件的屬性分兩種:資料屬性和訪問器屬性,每個型別的屬性都具有相應的特性。

    資料屬性:包含一個數據值的訪問位置,在這個位置可以讀取和寫入資料。其包含四種特性:[[Configurable]]、[[Enumerable]]、[[Writable]]、[[Value]]四個屬性;

    訪問器屬性:不包含資料值,它們包含一對兒getter和setter函式(不過,這兩個函式都不是必需的)。訪問器屬性不能直接定義,必須使用Object.defineProperty()來定義。其包含四種特性:[[Configurable]]、[[Enumerable]]、[[Get]]、[[Set]]。訪問器屬性常用的使用方式是設定一個屬性值導致其他屬性發生變化。

  2)使用Object.defineProperties()方法給物件同時定義多個屬性。

  3)使用Object.getOwnPropertyDescriptor()方法獲取給定屬性的特性。

2.建立物件

  1)工廠模式:通過函式,解決建立多個相似物件的問題,但是工廠模式沒有解決物件識別的問題(怎麼知道一個物件的型別)。

  2)建構函式模式:可以將其建立的例項標識為一種特定的型別,這正是建構函式模式勝過工廠模式的地方。(但是建構函式模式也有弊端,在建構函式模式包含函式屬性時,通過建構函式建立的不同物件都會有一個對應的函式屬性,完成相同的功能,但是這個函式屬性卻是通過Function函式定義的不同物件例項,這就說明建構函式模式建立的多個例項物件中總是包含完成相同功能但是卻佔用空間不同的Function例項,有些浪費。)

    丨當然我們有種方式可以避免這種弊端:(假如建構函式中需要建立的函式屬性名為:sayName),我們把sayName()

    丨函式的定義轉移到建構函式外部,而在構數內部,我們將sayName屬性設定成等於全域性的sayName()函式,這樣一來,

    丨由於sayName屬性包含的是一個指向函式的指標,因此不同物件就共享了在全域性作用域中定義的同一個sayName()函數了。

    丨但是這種方式也存在弊端,如果有很多方法,那麼就需要定義很多個全域性函式,這就完全失去了封裝性了。而這種問題可以通過原型模式來解決。

  3)原型模式:不在建構函式中定義物件例項的屬性資訊,而是將這些資訊直接新增到prototype的原型屬性值——原型物件中,這樣可以讓所有建立的物件例項共享它所包含的屬性和方法。

    a.理解原型物件:每一個函式都有一個prototype原型屬性,而這個屬性的屬性值為一個原型物件,而我們在該原型物件中定義建構函式需要定義的公有屬性,這樣在建構函式例項化物件時,所有的物件都會通過原型鏈訪問到這些公有的屬性,解決了建構函式模式的弊端。isPrototypeOf(測試一個原型物件是和物件例項的隱藏原型之間存在關係)、Object.getPrototypeOf()(獲取一個物件的隱藏原型)、Object.hasOwnProperty(屬性名)(判斷一個物件是否有自己對應的例項屬性)。 更具體的內容在我的另一系列部落格中已經講解了,不瞭解可以跳轉到(深入理解js原型和閉包)一文。

    b.查詢例項物件屬性和原型物件屬性的常用方法:in:判斷某個屬性是否能夠通過物件訪問,無論該屬性存在於例項中還是原型中;

                         for-in:返回物件所有可訪問的、可列舉的屬性,無論該屬性存在於例項中還是原型中。(但是IE8及更早版本,在例項中定義和原型中同名的屬性,且如果該屬性在原型中是不可列舉時,那該例項中該屬性也是不可返回的,其他瀏覽器正常);

                         Object.keys():獲取物件上所有可列舉的例項屬性;

                         Object.getOwnPropertyNames():獲取物件上所有例項屬性;

    c.更簡單的原型語法:每次使用“建構函式名.prototype”形式新增屬性太麻煩了,我們使用一個包含所有屬性和方法的物件字面量來重寫整個原型物件,減少不必要的輸入。(但這種方式有個小問題:那就是改變了constructor屬性的指向了,如果對constructor屬性有特殊需要,可以手動給constructor賦值為“建構函式名”,如果還覺得這樣會改變constructor的[[Enumerable]],屬性值,可以使用ES5中的Object.defineProperty()函式修改其屬性)。但是這種重寫原型物件的方式還存在一個弊端:例項物件的_proto_始終指向最初的原型物件,原型物件換為新的後,例項物件並不能找到它,那麼例項物件並不能獲取新原型物件中修改新增的屬性,如下圖所示:

             

    d.原型的動態性:可以隨時為原型新增屬性和方法,並且修改能夠立即在所有物件例項中反映出來(因為它是改變的指標指向)。但是其也存在c中所說的缺點。

    e.原生物件(Object、Array、String等等)的原型也是可以修改新增新屬性的,但是一般不建議修改。

    f.原型模式的弊端:1.原型模式省略了為建構函式傳遞初始化引數這一環節,結果所有例項在預設情況下都將取得相同的屬性值。(小問題)

               2.(大問題):由於原型中所有屬性是被大部分例項所共享的,這種共享對於函式非常合適,對於屬性值玩為基本值的屬性也可以,但是對於包含引用型別的屬性來說,問題比較突出了。因為有的時候我們想對某個例項的這個屬性單獨作出更改,但是對於引用型別的屬性會反映到其他例項屬性上,所以我們很少見到有人單數使用原型模式。

  4)組合使用建構函式模式和原型模式

      這是定義引用型別最建立的一種模式,也是目前在ES中使用最廣泛、認同度最高的一種模式。建構函式模式用於定義例項物件專屬屬性,而原型模式用於定義方法和共享的屬性。這樣建立的例項都會有自己的一份例項屬性副本,但同時又共享著對方的引用,最大限度地節省了記憶體。

  5)動態原型模式:在建構函式中初始化原型(僅在必要的情況下),通常是通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。

  6)寄生建構函式模式:這種模式只有在前述集中方法不適用的情況下使用,該方法類似於工廠模式,區別在於初始化例項是使用new操作符,這種方法也具有工廠模式的弊端,不能使用instance of來確定物件型別,一般不建議使用。

  7)穩妥建構函式模式:這種模式用於構造穩妥物件(穩妥物件:沒有公共屬性,而且其中方法也不引用this的物件。這種物件適合在一些安全的環境中(這些環境會禁止使用this和new),或者在防止資料被其他應用程式改動時使用)。這種模式與工程模式相似,有兩點不同:一是新建立物件的例項方法不引用this;二是不適用new操作符呼叫函式。同樣這種模式也有工廠模式的缺點,不能使用instance of來確定物件型別,一般只在有特殊安全要求的情況下使用。

3.繼承

  1)原型鏈:原型鏈是JS中實現繼承的主要方法。實現方式是重寫原型物件,代之以一個新型別的例項。比如A想繼承B的屬性和方法,則A.prototype=new B(),通過重寫A建構函式的原型物件,代之以B的一個例項,這樣A的所有例項中就可以繼承B的屬性和方法。

    a.預設的繼承:因為原型物件也是物件,所有物件都是是Object的例項,所以所有例項中都可以繼承訪問到Object定義的屬性和方法。

    b.確定例項和原型的關係:通過instace of,如果一個例項通過_proto_往上找,相應的函式通過prototype這條鏈往下找,能找到同一個物件引用,則instance of 返回值就是true。同時一個例項通過_proto_往上找的過程,就是原型鏈。

    c.覆蓋超型別中的方法或者新增超型別中不存在的方法時,一定要放在繼承之後。(覆蓋:因為如果放在前面,超型別的方法會覆蓋 它;新增新方法或者說定義自己的方法:因為繼承時,是替換子型別的prototype,會覆蓋原來原型中的方法,而之前我們說過,我們一般講方法定義在原型中)。

    d.原型鏈繼承的問題:1.會造成原型中存在引用型別值(如果父類屬性中包含引用型別值,重寫子類原型後會造成子類原型中包含引用型別屬性,造成該屬性共享,之前說過如果想每個例項都有自己的引用型別屬性,引用型別屬性是不能放在原型中的)。2.建立子類的例項時,不能在不影響所有物件例項的情況下向超型別的建構函式傳參。

  2)借用建構函式——在子類建構函式的內部呼叫超類建構函式。

    a.優勢:借用建構函式解決了原型鏈無法傳參的問題

    b.問題:借用建構函式也無法避免之前講過的建構函式模式存在的問題——方法屬性無法共享。同時父類原型中的屬性也無法繼承,要實現繼承父類只能之後建構函式模式,這顯然是不合理的,所以借用建構函式模式也很少單獨只用。

  3)組合繼承(偽經典繼承)

    使用原型鏈實現對原型屬性和方法的繼承,而通過借用建構函式實現對例項屬性的繼承。既通過在原型上定義方法實現了函式的複用,又能夠保證每個例項能夠擁有自己的屬性。是JS中最常見的繼承模式。

    問題:組合式繼承會造成兩次呼叫父類的建構函式,會在子類原型物件上創造不必要的多餘屬性,後面會講寄生組合式繼承,解決這一問題。

  4)原型式繼承(道格拉斯.克羅克福德 2006年提出的),本質是對給定物件的淺複製

    使用一個物件作為新物件的一個基礎利用objcet函式來建立新物件,ES5新增了Object.create()函式來規範化原型式繼承。在沒有必要興師動眾地建立建構函式,而只想讓一個物件與另一個物件保持類似的情況下可以使用原型式繼承。

    問題:因為使用原型重寫,所有包含引用型別值得屬性會共享。

  5)寄生式繼承——和寄生建構函式、工程模式類似,把原型式繼承過程封裝,並在內部以某種方式增強物件。(比原型式繼承做了一層封裝罷了)

    問題:使用寄生式為物件新增函式,不能做到函式複用。

  6)寄生組合式繼承

    利用寄生式繼承來建立超型別的一個副本來賦值給子類原型,而不必使用超類的建構函式,這樣只調用一次超類的建構函式,避免了在子類原型物件上建立不必要的屬性。寄生組合式繼承時引用型別最理想的繼承正規化。