歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> 深入理解JavaScript中創建對象模式的演變(原型)

深入理解JavaScript中創建對象模式的演變(原型)

日期:2017/3/1 9:10:00   编辑:Linux編程

創建對象的模式多種多樣,但是各種模式又有怎樣的利弊呢?有沒有一種最為完美的模式呢?下面我將就以下幾個方面來分析創建對象的幾種模式:

  • Object構造函數和對象字面量方法
  • 工廠模式
  • 自定義構造函數模式
  • 原型模式
  • 組合使用自定義構造函數模式和原型模式
  • 動態原型模式、寄生構造函數模式、穩妥構造函數模式

第一部分:Object構造函數和對象字面量方法

  我之前在博文《JavaScript中對象字面量的理解 http://www.linuxidc.com/Linux/2016-11/136666.htm》中講到過這兩種方法,如何大家不熟悉,可以點進去看一看回顧一下。它們的優點是用來創建單個的對象非常方便。但是這種方法有一個明顯的缺點:利用同一接口創建很多對象是,會產生大量的重復代碼。這句話怎麼理解呢?讓我們看一下下面的代碼:

1 2 3 4 var person1={ <strong>name</strong>:"zzw", <strong>age</strong>:"21", <strong>school</strong>:"xjtu",<br>         <strong> sayName</strong>:<strong>function(){</strong><br><strong>     console.log(this.name);</strong><br><strong> };</strong> 1 2 3 4 5 } var person2={ <strong>name</strong>:"ht", <strong>age</strong>:"18", <strong>school</strong>:"tjut",<br>         <strong> sayName:function(){</strong><br><strong>            console.log(this.name);</strong><br><strong>          };</strong><br><br> }

   可以看出,當我們創建了兩個類似的對象時,我們重復寫了name age school 以及對象的方法這些代碼,隨著類似對象的增多,顯然,代碼會凸顯出復雜、重復的感覺。為解決這一問題,工廠模式應運而生。

第二部分:工廠模式

  剛剛我們提到:為解決創建多個對象產生大量重復代碼的問題,由此產生了工廠模式。那麼,究竟什麼是工廠模式?它是如何解決這一問題的呢?首先,我們可以想一想何謂工廠? 就我個人理解:在工廠可以生產出一個模具,通過這個模具大量生產產品,最終我們可以加以修飾(比如噴塗以不同顏色,包裝不同的外殼)。這樣就不用一個一個地做產品,由此可以大大地提高效率。

  同樣地,對於創建對象也是這樣的思路:它會通過一個函數封裝創建對象的細節。最後直接將不同的參數傳遞到這個函數中去,以解決產生大量重復代碼的問題。觀察以下代碼:

1 2 3 4 5 6 7 8 9 10 11 12 13 function createPerson(name,age,school){ var o=new Object(); o.name=name; o.age=age; o.school=school; o.sayName=function(){ console.log(this.name); }; return o; } var person1=createPerson("zzw","21","xjtu"); var person2=createPerson("ht","18","tjut");

  看似這裡的代碼也不少啊!可是,如果在多創建2個對象呢,10個呢,100個呢?結果可想而知,於是工廠模式成功地解決了Object構造函數或對象字面量創建單個對象而造成大量代碼重復的問題!工廠模式有以下特點:

  • 在函數內部顯式地創建了對象。
  • 函數結尾一定要返回這個新創建的對象。

  但是,我們仔細觀察,可以發現工廠模式創建的對象,例如這裡創建的person1和person2,我們無法直接識別對象是什麼類型。為了解決這個問題,自定義的構造函數模式出現了。

第三部分:自定義構造函數模式

  剛剛說到,自定義構造函數模式是為了解決無法直接識別對象的類型才出現的。那麼顯然自定義構造函數模式至少需要解決兩個問題。其一:可以直接識別創建的對象的類型。其二:解決工廠模式解決的創建大量相似對象時產生的代碼重復的問題。

  那麼,我為什麼說是自定義構造函數模式呢?這是因為,第一部分中,我們使用的Object構造函數是原生構造函數���顯然它是解決不了問題的。只有通過創建自定義的構造函數,從而定義自定義對象類型的屬性和方法。代碼如下:

1 2 3 4 5 6 7 8 9 10 function Person(name,age,school){ this.name=name; this.age=age; this.school=school; this.sayName=function(){ console.log(this.name); }; } var person1=new Person("zzw","21","xjtu"); var person2=new Person("ht","18","tjut");

  首先我們驗證這種自定義的構造模式是否解決了第一個問題。在上述代碼之後追加下面的代碼:

1 2 console.log(person1 <strong>instanceof</strong> Person);//true console.log(person1 <strong>instanceof</strong> Object);//true

  結構都得到了true,對於Object當然沒有問題,因為一切對象都是繼承自Object的,而對於Person,我們在創建對象的時候用的是Person構造函數,那麼得到person1是Person類型的也就沒問題了。

  對於第二個問題,答案是顯而易見的。很明顯,創建大量的對象不會造成代碼的重復。於是,自定義構造函數成功解決所有問題。

  A 下面我們對比以下自定義構造函數與工廠模式的不同之處:

  • 自定義構造函數沒有用 var o = new Object()那樣顯式地創建對象
  • 與o.name等不同,它直接將屬性和方法賦給了this對象,this最終會指向新創建的對象。(this對象的更多細節可以在我的另一篇博文《JavaScript函數之美~》中查看)。
  • 因為沒有創建對象,所以最終沒有return一個對象(注意:構造函數在不返回值的情況下,會默認返回一個新對象實例)。

  B 對於構造函數,我們還應當注意:

  • 構造函數的函數名需要大寫,用以區分與普通函數。
  • 構造函數也是函數,只是它的作用之一是創建對象。
  • 構造函數在創建新對象時,必須使用new操作符。
  • 創建的兩個對象person1和person2的constructor(構造函數)屬性都指向用於創建它們的Person構造函數。

  C 如何理解構造函數也是函數?

  只要證明構造函數也可以像普通函數一樣的調用,那麼就可以理解構造函數也是函數了。

1 2 3 4 5 6 7 8 9 10 function Person(name,age,school){ this.name=name; this.age=age; this.school=school; this.sayName=function(){ console.log(this.name); }; } <strong>Person("zzw","21","xjtu");</strong> sayName();//zzw

  可以看出,我直接使用了Person("zzw","21","xjtu");來像普通函數一樣的調用這個構造函數,因為我們把它當作了普通函數,那麼函數中的this就不會指向之前所說的對象(這裡亦沒有對象),而是指向了window。於是,函數一經調用,內部的變量便會放到全局環境中去,同樣,對於其中的函數也會在調用之後到全局環境,只是這個內部的函數是函數表達式並未被調用。只有調用即sayName();才能正確輸出

  由此,我們證明了構造函數也是函數。

  D 那麼這種自定義構造函數就沒有任何問題嗎?

  構造函數的問題是在每次創建一個實例時,構造函數的方法都需要再實例上創建一遍。由於在JavaScript中,我們認為所有的函數(方法)都是對象,所以每當創建一個實例對象,都會同時在對象的內部創建一個新的對象(這部分內容同樣可以在我的博文《JavaScript函數之美~》中找到)。即我們之前創建的自定義構造函數模式相當於下列代碼:

1 2 3 4 5 6 function Person(name,age,school){ this.name=name; this.age=age; this.school=school; this.sayName=new Function("console.log(this.name)"); } ? 1 2 var person1=new Person("zzw","21","xjtu"); var person2=new Person("ht","18","tjut");

  

  即我們在創建person1和person2的時候,同時創建了兩個sayName為對象指針的對象,我們可以通過下面這個語句做出判斷:

1 console.log(person1.sayName==person2.sayName);//false

  這就證明了如果創建兩個對象同時也在每個對象中又各自創建了一個函數對象,但是創建兩個完成同樣任務的Function實例的確沒有必要(況且內部有this對象,只要創建一個對象,this便會指向它)。這就造成了內部方法的重復造成資源浪費。

  E 解決方法。

  如果我們將構造函數內部的方法放到構造函數的外部,那麼這個方法便會被person1和person2共享了,於是,在每次創建新對象時就不會同時創建這個方法對象了。如下:

1 2 3 4 5 6 7 8 9 10 11 function Person(name,age,school){ this.name=name; this.age=age; this.school=school; this.sayName=sayName; } <strong>function sayName(){ console.log(this.name); }</strong> var person1=new Person("zzw","21","xjtu"); var person2=new Person("ht","18","tjut");

person1.sayName();//zzw

  應當注意:this.sayName=sayName;中這裡等式右邊的sayName是一個指針,所以在創建新對象的時候只是創建了一個指向共同對像那個的指針而已,並不會創建一個方法對象。這樣便解決了問題。 而外面的sayName函數在最後一句中是被對象調用的,所以其中的this同樣是指向了對象。

  

F新的問題

如果這個構造函數中需要的方法很多,那麼為了保證能夠解決E中的問題,我們需要把所有的方法都寫在構造函數之外,可是如果這樣:

  1. 在全局作用域中定義的函數從未在全局環境中調用,而只會被某個對象調用,這樣就讓全局作用域有點名不副實。
  2. 如果把所有構造函數中的方法都放在構造函數之外,這樣就沒有封裝性可言了。   

  由此,為了解決F中的問題,接下來不得不提到JavaScript語言中的核心原型模式了。

第四部分:原型模式

為什麼會出現原型模式呢?這個模式在上面講了是為了解決自定義構造函數需要將方法放在構造函數之外造成封裝性較差的問題。當然它又要解決構造函數能夠解決的問題,所以,最終它需要解決以下幾個問題。其一:可以直接識別創建的對象的類型。其二:解決工廠模式解決的創建大量相似對象時產生的代碼重復的問題。其三:解決構造函數產生的封裝性不好的問題。由於這個問題比較復雜,所以我會分為幾點循序漸進的做出說明。

A 理解原型對象

   首先,我們應當知道:無論什麼時候,只要創建了一個新函數(函數即對象),就會根據一組特定的規則創建一個函數(對象)的prototype屬性理解為指針,這個屬性會指向函數的原型對象(原型對象也是一個對象),但是因為我們不能通過這個新函數訪問prototype屬性,所以寫為[[prototype]]。同時,對於創建這個對象的構造函數也將獲得一個prototype屬性(理解為指針,同時指向它所創建的函數(對象)所指向的原型對象,這個構造函數是可以直接訪問prototype屬性的,所以我們可以通過訪問它將定義對象實例的信息直接添加到原型對象中。這時原型對象擁有一個constructor屬性(理解為指針)指向創建這個對象的構造函數(注意:這個constructor指針不會指向除了構造函數之外的函數)。

   你可能會問?所有的函數都是由構造函數創建的嗎?答案是肯定的。函數即對象,我在博文《JavaScript函數之美~》中做了詳盡介紹。對與函數聲明和函數表達式這樣建立函數的方法本質上也是由構造函數創建的。

  

   上面的說法可能過於抽象,我們先寫出一個例子(這個例子還不是我們最終想要的原型模式,只是為了讓大家先理解原型這個概念),再根據代碼作出說明:

1 2 3 4 5 6 7 8 9 10 11 12 <strong> function Person(){}</strong> Person.prototype.name="zzw"; Person.prototype.age=21; Person.prototype.school="xjtu"; Person.prototype.sayName=function(){ console.log(this.name); }; var person1=new Person(); var person2=new Person(); person1.sayName();//zzw person2.sayName();//zzw console.log(person1.sayName==person2.sayName);//true

  在這個例子中,我們首先創建了一個內容為空的構造函數,因為剛剛講了我們可以通過訪問構造函數的prototype屬性來為原型對象中添加屬性和方法。於是在下面幾行代碼中,我們便通過訪問構造函數的prototype屬性向原型對象中添加了屬性和方法。接著,創建了兩個對象實例person1和person2,並調用了原型對象中sayName()方法,得到了原型對象中的name值。這說明:構造函數創建的每一個對象和實例都擁有或者說是繼承了原型對象的屬性和方法。(因為無論是創建的對象實例還是創造函數的prototype屬性都是指向原型對象的) 換句話說,原型對象中的屬性和方法會被構造函數所創建的對象實例所共享,這也是原型對象的一個好處。

  下面我會畫一張圖來繼續闡述這個問題:

從這張圖中我們可以看出以下幾點:

  1. 構造函數和由構造函數創建的對象的prototype指針都指向原型對象。即原型對象既是構造函數的原型對象,又是構造函數創建的對象的原型對象。
  2. 原型對象有一個constructor指針指向構造函數,卻不會指向構造函數創建的實例。
  3. 構造函數的實例的[[prototype]]屬性被實例訪問來添加或修改原型對象的屬性和方法的,而構造函數的prototype屬性可以被用來訪問以修改原型對象的屬性和方法。
  4. person1和person2與他們的構造函數之間沒有直接的關系,只是他們的prototype屬性同時指向了同一個原型對象而已。
  5. Person.prototype指向了原型對象,而Person.prototype.constructor又指回了Person。
  6. 雖然這兩個實例都不包含屬性和方法,但我們卻可以調用person1.name,這是通過查找對象屬性的過程來實現的。

B.有關於原型對象中的方法以及實例中的屬性和原型對象中的屬性

為了加深對原型的理解,我在這裡先介紹兩種方法確定構造函數創建的實例對象與原型對象之間的關系。

  第一種方法:isPrototypeOf()方法,通過原型對象調用,確定原型對象是否是某個實例的原型對象。在之前的代碼後面追加下面兩句代碼:

1 2 console.log(Person.prototype.isPrototypeOf(person1));//true console.log(Person.prototype.isPrototypeOf(person2));//true

  結果不出意外地均為true,也就是說person1實例和person2實例的原型對象都是Person.prototype。

  第二種方法:Object.getPrototypeOf()方法,通過此方法得到某個對象實例的原型。在之前的代碼後面追加下面三句代碼:

1 2 console.log(Object.getPrototypeOf(person1)); console.log(Object.getPrototypeOf(person1)==Person.prototype);<br>          console.log(Object.getPrototypeOf(person1).name);//zzw

  其中第一句代碼在控制台中可以直接獲得person1的原型對象,如下圖所示:

其中第二句代碼得到布爾值:true。第三句代碼得到了原型對象中的name屬性值。

但是,當實例自己本身有和原型中相同的屬性名,而屬性值不同,在代碼獲取某個對象的屬性時,該從哪裡獲取呢?

  規則是:在代碼讀取某個對象而某個屬性是,都會執行一次搜索,目標是具有給定名字的屬性。搜索首先從實例本身開始,如果在實例中找到了給定名字的屬性,則返回該屬��的值;如果沒有找到,則繼續搜索指針指向的原型對象。觀察下面的例子。

1 2 3 4 5 6 7 8 9 10 11 12 13 function Person(){} Person.prototype.name="zzw"; Person.prototype.age=21; Person.prototype.school="xjtu"; Person.prototype.sayName=function(){ console.log(this.name); }; var person1=new Person(); var person2=new Person(); console.log(person1.name);//zzw <strong>person1.name="htt";</strong> <strong>console.log(person1.name);//htt</strong> console.log(person2.name);//zzw<br>        <strong>  delete</strong> person1.name;<br> <strong> console.log(person1.name);//zzw</strong><br>
  • 首先,我們把person1實例的name屬性設置為"htt" ,當我們直接獲取person1的name屬性時,會現在person1本身找該屬性(理解為就近原則),找不到,繼續向原型對象中尋找。
  • 當給person1對象添加了自身的屬性name時,這次得到的時person1自身的屬性,即該屬性屏蔽了原型中的同名屬性。
  • 通過倒數第三句代碼再次得到了zzw,這說明我們對person1設定了與原型對象相同的屬性名,但卻沒有重寫原型對象中的同名屬性。
  • 最後,我們可以通過delete刪除實例中的屬性,而原型中的屬性不會被刪除。

  第三種方法:hasOwnProperty()方法

   該方法可以檢測一個屬性是存在於實例中還是存在於原型中。只有給定屬性存在於對象實例中時,才會返回true,否則返回false。舉例如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function Person(){} Person.prototype.name="zzw"; Person.prototype.age=21; Person.prototype.school="xjtu"; Person.prototype.sayName=function(){ console.log(this.name); }; var person1=new Person(); var person2=new Person(); console.log(person1.name);//zzw <strong> console.log(person1.hasOwnProperty("name"));//false 因為zzw是搜索於原型對象的</strong> person1.name="htt"; console.log(person1.name);//htt <strong> console.log(person1.hasOwnProperty("name"));//true 在上上一句,我添加了person1實例的屬性,它不是屬於原型對象的屬性</strong> delete person1.name; console.log(person1.name);//zzw <strong> console.log(person1.hasOwnProperty("name"));//false 由於使用delete刪除了實例中的name屬性,所以為false </strong>

C.in操作符的使用以及如何編寫函數判斷屬性存在於對象實例中

  in操作符會在通過對象能夠訪問給定屬性時,返回true,無論該屬性存在於事例中還是原型中。觀察下面的例子:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function Person(){} Person.prototype.name="zzw"; Person.prototype.age=21; Person.prototype.school="xjtu"; Person.prototype.sayName=function(){ console.log(this.name); }; var person1=new Person(); var person2=new Person(); console.log(person1.name);//zzw console.log(person1.hasOwnProperty("name"));//false <strong>console.log("name" in person1);//true</strong> person1.name="htt"; console.log(person1.name);//htt console.log(person1.hasOwnProperty("name"));//true <strong>console.log("name" in person1);//true</strong> delete person1.name; console.log(person1.name);//zzw console.log(person1.hasOwnProperty("name"));//false <strong> console.log("name" in person1);//true </strong>

  可以看到,確實,無論屬性在實例對象本身還是在實例對象的原型對象都會返回true。

  有了in操作符以及hasOwnProperty()方法我們就可以判斷一個屬性是否存在於原型對象了(而不是存在於對象實例或者是根本就不存在)。編寫hasPrototypeProperty()函數並檢驗:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function Person(){} <strong> function hasPrototypeProperty(Object,name){ return !Object.hasOwnProperty(name)&&(name in Object); }</strong> Person.prototype.name="zzw"; Person.prototype.age=21; Person.prototype.school="xjtu"; Person.prototype.sayName=function(){ console.log(this.name); }; var person1=new Person(); var person2=new Person(); console.log(person1.name);//zzw <strong>console.log(hasPrototypeProperty(person1,"name"));//true</strong> person1.name="htt"; console.log(person1.name);//htt <strong> console.log(hasPrototypeProperty(person1,"name"));//true</strong> delete person1.name; console.log(person1.name);//zzw <strong> console.log(hasPrototypeProperty(person1,"name"));//true </strong>

  其中hasPrototypeProperty()函數的判斷方式是:in操作符返回true而hasOwnProperty()方法返回false,那麼如果最終得到true則說明屬性一定存在於原型對象中。(注意:邏輯非運算符!的優先級要遠遠高於邏輯與&&運算符的優先級

D.for-in循環和Object.keys()方法在原型中的使用

  在通過for-in循環時,它返回的是所有能夠通過對象訪問的、可枚舉的屬性,其中既包括存在於實例中的屬性,也包括存在於原型中的屬性。且對於屏蔽了原型中不可枚舉的屬性(即將[[Enumerable]]標記為false的屬性)也會在for-in中循環中返回。(注:IE早期版本中存在一個bug,即屏蔽不可枚舉屬性的實例屬性不會出現在for-in循環中,這裡不做詳細介紹)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function Person(){}<br>          Person.prototype.name="zzw"; Person.prototype.age=21; Person.prototype.school="xjtu"; Person.prototype.sayName=function(){ console.log(this.name); }; var person1=new Person(); var person2=new Person(); console.log(person1.name);//zzw person1.name="htt"; console.log(person1.name);//htt delete person1.name; console.log(person1.name);//zzw for(var propName in person1){ console.log(propName);//name age school sayName }

  通過for-in循環,我們可以枚舉初name age school sayName這幾個屬性。由於person1中的[[prototype]]屬性不可被訪問,因此,我們不能利用for-in循環枚舉出它。

  Object.keys()方法接收一個參數,這個參數可以是原型對象,也可以是由構造函數創建的實例對象,返回一個包含所有可枚舉屬性的字符串數組。如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function Person(){} Person.prototype.name="zzw"; Person.prototype.age=21; Person.prototype.school="xjtu"; Person.prototype.sayName=function(){ console.log(this.name); }; var person1=new Person(); var person2=new Person(); console.log(person1.name);//zzw person1.name="htt"; console.log(person1.name);//htt person1.age="18"; <strong> console.log(Object.keys(Person.prototype));//["name", "age", "school", "sayName"] console.log(Object.keys(person1));//["name", "age"] console.log(Object.keys(person2));//[] </strong>

  

  我們可以從上面的例子中看到,Object.keys()方法返回的是其自身的屬性。如原型對象只返回原型對象中的屬性,對象實例也只返回對象實例自己創建的屬性,而不返回繼承自原型對象的實例。

E 更簡單的原型語法

  在之前的例子中,我們在構造函數的原型對象中添加屬性和方法時,每次都要在前面敲一遍Person.prototype,如果屬性多了,這樣的方法會顯得更為繁瑣,那麼下面我將介紹給大家一種簡單的方法。

  我們知道,原型對象說到底它還是個對象,只要是個對象,我們就可以使用對象字面量方法來創建,方法如下:

1 2 3 4 5 6 7 8 9 function Person(){} Person.prototype={ name:"zzw", age:21, school:"xjtu", sayName:function (){ console.log(this.name); } };//原來利用Person.prototype.name="zzw"知識對象中的屬性,對於對象並沒有任何影響,而這裡創建了新的對象<br>          

  同樣,最開始,我們創建一個空的Person構造函數(大家發現了沒有,其實每次我們創建的都是空的構造函數),然後用對象字面量的方法來向原型對象中添加屬性。這樣既減少了不必要的輸入,也從視覺上更好地封裝了原型。 但是,這時原型對象的constructor就不會指向Person構造函數而是指向Object構造函數了。

  為什麼會這樣?我們知道,當我們創建Person構造函數時,就會同時自動創建這個Person構造函數的原型(prototype)對象,這個原型對象也自動獲取了一個constructor屬性並指向Person構造函數,這個之前的圖示中可以清楚地看出來。之前我們使用的較為麻煩的方法(e.g. Person.prototype.name="zzw")只是簡單地向原型對象添加屬性,並沒有其他本質的改變。然而,上述這種封裝性較好的方法即使用對象字面量的方法,實際上是使用Object構造函數創建了一個新的原型對象(對象字面量本質即利用Object構造函數創建新對象),注意:此時Person構造函數的原型對象不再是之前的原型對象(而之前的原型對象的constructor屬性仍然指向Person構造函數),而和Object構造函數的原型對象一樣均為這個新的原型對象。這個原型對象和創建Person構造函數時自動生成的原型對象風馬牛不相及。理所應當的是,對象字面量創建的原型對象的constructor屬性此時指向了Object構造函數。

我們可以通過下面幾句代碼來驗證:

1 2 3 4 5 6 7 8 9 10 11 12 function Person(){} Person.prototype={ name:"zzw", age:21, school:"xjtu", sayName:function (){ console.log(this.name); } }; var person1=new Person(); console.log(Person.prototype.constructor==Person);//false console.log(Person.prototype.constructor==Object);//true

  通過最後兩行代碼我們可以看出Person構造函數的原型對象的constructor屬性此時不再指向Person構造函數,而是指向了Object構造函數。但是這並被影響我們正常使用,下面幾行代碼便可以清楚地看出:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 function Person(){} Person.prototype={ name:"zzw", age:21, school:"xjtu", sayName:function (){ console.log(this.name); } }; var person1=new Person(); console.log(person1.name);//zzw console.log(person1.age);//21 console.log(person1.school);//xjtu person1.sayName();//zzw

  下面我將以個人的理解用圖示表示(如果有問題,請指出):

    第一步:創建一個空的構造函數。function Person(){}。此時構造函數的prototype屬性指向原型對象,而原型對象的constructor屬性指向Person構造函數。

  第二步:利用對象字面量的方法創建一個Person構造函數的新原型對象。

1 2 3 4 5 6 7 8   Person.prototype={ name:"zzw", age:21, school:"xjtu", sayName:function (){ console.log(this.name); } };

此時,由於創建了Person構造函數的一個新原型對象,所以Person構造函數的prototype屬性不再指向原來的原型對象,而是指向了Object構造函數創建的原型對象(這是對象字面量方法的本質)。但是原來的原型對象的constructor屬性仍指向Person構造函數。

   第三步:由Person構造函數創建一個實例對象。

這個對象實例的constructor指針同構造它的構造函數一樣指向新的原型對象。

  總結:從上面的這個例子可以看出,雖然新創建的實例對象仍可以共享添加在原型對象裡面的屬性,但是這個新的原型對象卻不再指向Person構造函數而指向Object構造函數,如果constructor的值真的非常重要的時候,我們可以像下面的代碼這樣重新設置會適當的值:

1 2 3 4 5 6 7 8 9 10 function Person(){} Person.prototype={ constructor:Person, name:"zzw", age:21, school:"xjtu", sayName:function (){ console.log(this.name); } };

  這樣,constructor指針就指回了Person構造函數。即如下圖所示:

  值得注意的是:這種方式重設constructor屬性會導致它的[[Enumerable]]特性設置位true,而默認情況下,原生的constructor屬性是不可枚舉的。但是我們可以試用Object.defineProperty()將之修改為不可枚舉的(這一部分可以參見我的另一篇博文:《深入理解JavaScript中的屬性和特性》)。

F.原生對象的原型

  原型的重要性不僅體現在自定義類型方面,就連所有原生的引用類型,都是使用這種模式創建的。所有原生引用類型(Object、Array、String,等等)都在其構造函數的原型上定義了方法。例如在Array.prototype中可以找到sort()方法,而在String.prototype中就可以找到substring()方法。

1 2 console.log(typeof Array.prototype.sort);//function console.log(typeof String.prototype.substring);//function

  於是,實際上我們是可以通過原生對象的原型來修改它。比如:   

1 2 3 4 5 String.prototype.output=function (){ alert("This is a string"); } var message="zzw"; message.output();

  這是,便在窗口中彈出了“This is a string”。盡管可以這樣做,但是我們不推薦在產品化的程序中修改原生對象的原型。這樣做有可能導致命名沖突等問題。

G.原型模式存在的問題

  實際上,從上面對原型的講解來看,原型模式還是有很多問題的,它並沒有很好地解決我在第四部分初提出的若干問題:“其一:可以直接識別創建的對象的類型。其二:解決工廠模式解決的創建大量相似對象時產生的代碼重復的問題。其三:解決構造函數產生的封裝性不好的問題。”其中第一個問題解決的不錯,通過構造函數便可以直接看出來類型。第二個問題卻解決的不好,因為它省略了為構造函數傳遞初始化參數這一環節,結果所有的實例在默認情況下都將取得相同的默認值,我們只能通過在實例上添加同名屬性來屏蔽原型中的屬性,這無疑也會造成代碼重復的問題。第三個問題,封裝性也還說的過去。因此原型模式算是勉強解決了上述問題。

  但是這種方法還由於本身產生了額外的問題。看下面的例子:

? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function Person(){} Person.prototype={ constructor:Person, name:"zzw", age:21, school:"xjtu", friends:["pengnian","zhangqi"], sayName:function (){ console.log(this.name); } }; var person1=new Person(); var person2=new Person(); person1.friends.push("feilong"); console.log(person1.friends);//["pengnian","zhangqi","feilong"] console.log(person2.friends);//["pengnian","zhangqi","feilong"]

  這裡我在新建的原型對象中增加了一個數組,於是這個數組會被後面創建的實例所共享,但是person1.friends.push("feilong");這句代碼我的意思是添加為person1的朋友而不是person2的朋友,但是在結果中我們可以看到person2的朋友也有了feilong,這就不是我們所希望的了。這也是對於包含引用類型的屬性的最大問題。

  也正是這個問題和剛剛提到的第二個問題(即它省略了為構造函數傳遞初始化參數這一環節,結果所有的實例在默認情況下都將取得相同的默認值,我們只能通過在實例上添加同名屬性來屏蔽原型中的屬性,這無疑也會造成代碼重復的問題),很少有人會單單使用原型模式。

第五部分:組合使用自定義構造函數模式和原型模式

  剛剛我們說到的原型模式存在的兩個最大的問題。問題一:由於沒有在為構造函數創建對象實例時傳遞初始化參數,所有的實例在默認情況下獲取了相同的默認值。問題二:對於原型對象中包含引用類型的屬性,在某一個實例中修改引用類型的值,會牽涉到其他的實例,這不是我們所希望的。而組合使用自定義構造函數模式和原型模式即使構造函數應用於定義實例屬性,而原型模式用於定義方法和共享的屬性。它能否解決問題呢?下面我們來一探究竟!

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function Person(name,age,school){ this.name=name; this.age=age; this.school=school; this.friends=["pengnian","zhangqi"]; } Person.prototype={ constructor:Person, sayName:function(){ console.log(this.name); } } var person1=new Person("zzw",21,"xjtu"); var person2=new Person("ht",18,"tjut"); person1.friends.push("feilong"); console.log(person1.friends);//["pengnian", "zhangqi", "feilong"] console.log(person2.friends);//["pengnian", "zhangqi"] console.log(person1.sayName==person2.sayName);//true

  OK!我們來看看組合使用構造函數模式和原型模式解決的問題:

  1. 解決了Object構造函數和對象字面量方法在創建大量對象時造成的代碼重復問題(因為只要在創建對象時向構造函數傳遞參數即可)。
  2. 解決了工廠模式產生的無法識別對象類型的問題(因為這裡通過構造函數即可獲知對象類型)。
  3. 解決了自定義構造函數模式封裝性較差的問題(這裡全部都被封裝)。
  4. 解決了原型模式的兩個問題:所有實例共享相同的屬性以及包含引用類型的數組在實例中修改時會影響原型對象中的數組。

  綜上所述,組合使用構造函數模式和原型模式可以說是非常完美了。

第六部分:動態原型模式、寄生構造函數模式、穩妥構造函數模式

  實際上,組合使用構造函數模式和原型模式確實已經非常完美了,這裡將要講的幾種模式都是在特定的情況下使用的,所以我認為第六部分相對於第五部分並沒有進一步的提高。僅僅是多學習幾種模式可以解決更多的問題。

A 動態原型模式

  這裡的動態原型模式相對於第五部分的組合使用自定義構造函數模式和原型模式本質上是沒有什麼差別的,只是因為對於有其他OO(Object Oriented,面向對象)語言經驗的開發人員看到這種模式會覺得奇怪,因此我們可以將所有信息都封裝在構造函數中。本質上是通過檢測某個應該存在的方法是否存在或有效,來決定是否要初始化原型。如下例所示:

1 2 3 4 5 6 7 8 9 10 11 12 13 function Person(name,age,school){ this.name=name; this.age=age; this.school=school; if(typeof this.sayName != "function"){ Person.prototype.sayName=function(){ console.log(this.name); }; } } var person=new Person("zzw",21,"xjtu");//使用new調用構造函數並創建一個實例對象 person.sayName(); //zzw console.log(person.school);//xjtu

  這裡先聲明了一個構造函數,然後當使用new操作符調用構造函數創建實例對象時進入了構造函數的函數執行環境,開始檢測對象的sayName是否存在或是否是一個函數,如果不是,就使用原型修改的方式向原型中添加sayName函數。且由於原型的動態性,這裡所做的修改可以在所有實例中立即得到反映。值得注意的是在使用動態原型模式時,不能使用對象字面量重寫原型,否則,在建立了實例的情況下重寫原型會導致切斷實例和新原型的聯系。

B 寄生構造函數模式

  寄生構造函數模式是在前面幾種模式都不適用的情況下使用的。看以下例子,再做出說明:

1 2 3 4 5 6 7 8 9 10 11 12 function Person(name,age,school){ var o =new Object(); o.name=name; o.age=age; o.school=school; o.sayName=function(){ console.log(this.name); }; return o; } var person = new Person("zzw",21,"xjtu"); person.sayName();//zzw

  寄生構造函數的特點如下:

  • 聲明一個構造函數,在構造函數內部創建對象,最後返回該對象,因此這個函數的作用僅僅是封裝創建對象的代碼。
  • 可以看出,這種方式除了在創建對象的時候使用了構造函數的模式(函數名大寫,用new關鍵字調用)以外與工廠模式一模一樣。
  • 構造函數在不返回值的情況下,默認會返回新對象實例,而通過構造函數的末尾添加一個return語句,可以重寫調用構造函數時返回的值。

  這個模式可以在特殊的情況下來為對象創建構造函數。假設我們想要創建一個具有額外方法的特殊數組,通過改變Array構造函數的原型對象是可以實現的,但是我在第四部分F中提到過,這種方式可能會導致後續的命名沖突等一系列問題,我們是不推薦的。而寄生構造函數就能很好的解決這一問題。如下所示:

1 2 3 4 5 6 7 8 9 10 function SpecialArray(){ var values=new Array(); values.push.apply(values,arguments); values.toPipedString=function(){ return this.join("|"); }; return values; } var colors=new SpecialArray("red","blue","green"); console.log(colors.toPipedString());//red|blue|green

  或者如下所示:

1 2 3 4 5 6 7 8 9 10 function SpecialArray(string1,string2,string3){ var values=new Array(); values.push.call(values,string1,string2,string3); values.toPipedString=function(){ return this.join("|"); }; return values; } var colors=new SpecialArray("red","blue","green"); console.log(colors.toPipedString());//red|blue|green

  這兩個例子實際上是一樣的,唯一差別在於call()方法和apply()方法的應用不同。(這部分內容詳見《JavaScript函數之美~》)

  這樣就既沒有改變Array構造函數的原型對象,又完成了添加Array方法的目的。

  關於寄生構造函數模式,需要說明的是:返回的對象與構造函數或構造函數的原型屬性之間沒有任何關系;也就是說,構造函數返回的對象在與構造函數外部創建的對象沒有什麼不同。故不能依賴instanceof來確定對象類型。於是,我們建議在可以使用其他模式創建對象的情況下不使用寄生構造函數模式。

C.穩妥構造函數模式

  穩妥對象是指這沒有公共屬性,而且方法也不引用this的對象。穩妥對象適合在安全的環境中使用,或者在防止數據被其他應用程序改動時使用。舉例如下:

1 2 3 4 5 6 7 8 9 function Person(name,age,school){ var o=new Object(); o.sayName=function (){ console.log(name); }; return o; } var person=Person("zzw",21,"xjtu"); person.sayName();//zzw

  可以看出來,這種模式和寄生構造函數模式非常相似,只是:

  1.新創建對象的實例方法不用this。

  2.不用new操作符調用構造函數(由函數名的首字母大寫可以看出它的確是一個構造函數)。

  注意:變量person中保存的是一個穩妥對象,除了調用sayName()方法外沒有別的方式可以訪問其數據成員。例如在上述代碼下添加:

1 2 console.log(person.name);//undefined console.log(person.age);//uncefined

  因此,穩妥構造函數模式提供的這種安全性,使得它非常適合在某些安全執行環境提供的環境下使用。

第七部分:總結

  在這篇博文中,在創建大量相似對象的前提下,我以分析各種方法利弊的思路下向大家循序漸進地介紹了Object構造函數和對象字面量方法、工廠模式、自定義構造函數模式、原型模式、組合使用自定義構造函數模式和原型模式、動態原型模式、寄生構造函數模式、穩妥構造函數模式這幾種模式,其中我認為組合使用自定義構造函數模式和原型模式以及動態原型模式都是非常不錯的模式。而對於創建對象數量不多的情況下,對象字面量方法、自定義構造函數模式也都是不錯的選擇。

  這一部分內容屬於JavaScript中的重難點,希望大家多讀幾遍,相信一定會有很大的收獲!

  在寫這篇博文的過程中,設及知識點較多,錯誤在所難免,希望大家批評指正。

Copyright © Linux教程網 All Rights Reserved