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

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

  

  建立對象的模式多種多樣,可是各類模式又有怎樣的利弊呢?有沒有一種最爲完美的模式呢?下面我將就如下幾個方面來分析建立對象的幾種模式:javascript

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

  我以前在博文《javascript中對象字面量的理解》中講到過這兩種方法,如何你們不熟悉,能夠點進去看一看回顧一下。它們的優勢是用來建立單個的對象很是方便。可是這種方法有一個明顯的缺點:利用同一接口建立不少對象是,會產生大量的重複代碼。這句話怎麼理解呢?讓咱們看一下下面的代碼:html

	var person1={
		name:"zzw",
		age:"21",
		school:"xjtu",
          sayName:function(){
    console.log(this.name);
};
        }
	var person2={
		name:"ht",
		age:"18",
		school:"tjut",
          sayName:function(){
            console.log(this.name);
          };

}

   能夠看出,當咱們建立了兩個相似的對象時,咱們重複寫了name age school 以及對象的方法這些代碼,隨着相似對象的增多,顯然,代碼會凸顯出複雜、重複的感受。爲解決這一問題,工廠模式應運而生。java

 

第二部分:工廠模式

  剛剛咱們提到:爲解決建立多個對象產生大量重複代碼的問題,由此產生了工廠模式。那麼,究竟什麼是工廠模式?它是如何解決這一問題的呢?首先,咱們能夠想想何謂工廠? 就我我的理解:在工廠能夠生產出一個模具,經過這個模具大量生產產品,最終咱們能夠加以修飾(好比噴塗以不一樣顏色,包裝不一樣的外殼)。這樣就不用一個一個地作產品,由此能夠大大地提升效率。數組

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

    	       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構造函數或對象字面量建立單個對象而形成大量代碼重複的問題!工廠模式有如下特色:app

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

  可是,咱們仔細觀察,能夠發現工廠模式建立的對象,例如這裏建立的person1和person2,咱們沒法直接識別對象是什麼類型。爲了解決這個問題,自定義的構造函數模式出現了。函數

 

 

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

  剛剛說到,自定義構造函數模式是爲了解決沒法直接識別對象的類型纔出現的。那麼顯然自定義構造函數模式至少須要解決兩個問題。其一:能夠直接識別建立的對象的類型。其二:解決工廠模式解決的建立大量類似對象時產生的代碼重複的問題。學習

  那麼,我爲何說是自定義構造函數模式呢?這是由於,第一部分中,咱們使用的Object構造函數是原生構造函數,顯然它是解決不了問題的。只有經過建立自定義的構造函數,從而定義自定義對象類型的屬性和方法。代碼以下:this

		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");

  

 

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

		console.log(person1 instanceof Person);//true
		console.log(person1 instanceof 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 如何理解構造函數也是函數?

  只要證實構造函數也能夠像普通函數同樣的調用,那麼就能夠理解構造函數也是函數了。

		function Person(name,age,school){
			this.name=name;
			this.age=age;
			this.school=school;
			this.sayName=function(){
				console.log(this.name);
			};
		}
		Person("zzw","21","xjtu");
		sayName();//zzw

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

  由此,咱們證實了構造函數也是函數。

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

  構造函數的問題是在每次建立一個實例時,構造函數的方法都須要再實例上建立一遍。因爲在JavaScript中,咱們認爲全部的函數(方法)都是對象,因此每當建立一個實例對象,都會同時在對象的內部建立一個新的對象(這部份內容一樣能夠在個人博文《JavaScript函數之美~》中找到)。即咱們以前建立的自定義構造函數模式至關於下列代碼:

 

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

  

  即咱們在建立person1和person2的時候,同時建立了兩個sayName爲對象指針的對象,咱們能夠經過下面這個語句作出判斷:

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

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

  E 解決方法。

  若是咱們將構造函數內部的方法放到構造函數的外部,那麼這個方法便會被person1和person2共享了,因而,在每次建立新對象時就不會同時建立這個方法對象了。以下:

	       function Person(name,age,school){
			this.name=name;
			this.age=age;
			this.school=school;
                        this.sayName=sayName;
		}
		function sayName(){
			console.log(this.name);
		}
		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函數之美~》中作了詳盡介紹。對與函數聲明和函數表達式這樣創建函數的方法本質上也是由構造函數建立的。

     

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

		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();
		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()方法,經過原型對象調用,肯定原型對象是不是某個實例的原型對象。在以前的代碼後面追加下面兩句代碼:

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

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

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

		console.log(Object.getPrototypeOf(person1));
		console.log(Object.getPrototypeOf(person1)==Person.prototype);
          console.log(Object.getPrototypeOf(person1).name);//zzw

  其中第一句代碼在控制檯中能夠直接得到person1的原型對象,以下圖所示:

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

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

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

		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
		console.log(person2.name);//zzw
          delete person1.name;
console.log(person1.name);//zzw
  •  首先,咱們把person1實例的name屬性設置爲"htt" ,當咱們直接獲取person1的name屬性時,會如今person1自己找該屬性(理解爲就近原則),找不到,繼續向原型對象中尋找。
  •  當給person1對象添加了自身的屬性name時,此次獲得的時person1自身的屬性,即該屬性屏蔽了原型中的同名屬性。
  •  經過倒數第三句代碼再次獲得了zzw,這說明咱們對person1設定了與原型對象相同的屬性名,但卻沒有重寫原型對象中的同名屬性。
  •  最後,咱們能夠經過delete刪除實例中的屬性,而原型中的屬性不會被刪除。 

 

  第三種方法:hasOwnProperty()方法

   該方法能夠檢測一個屬性是存在於實例中仍是存在於原型中。只有給定屬性存在於對象實例中時,纔會返回true,不然返回false。舉例以下:

		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  由於zzw是搜索於原型對象的
		person1.name="htt";
		console.log(person1.name);//htt 
		console.log(person1.hasOwnProperty("name"));//true 在上上一句,我添加了person1實例的屬性,它不是屬於原型對象的屬性
	        delete person1.name;
                console.log(person1.name);//zzw
                console.log(person1.hasOwnProperty("name"));//false  因爲使用delete刪除了實例中的name屬性,因此爲false

  

 

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

  in操做符會在經過對象可以訪問給定屬性時,返回true,不管該屬性存在於事例中仍是原型中。觀察下面的例子:

		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
		console.log("name" in person1);//true
		person1.name="htt";
		console.log(person1.name);//htt
		console.log(person1.hasOwnProperty("name"));//true
		console.log("name" in person1);//true
	        delete person1.name;
                console.log(person1.name);//zzw
                console.log(person1.hasOwnProperty("name"));//false
                console.log("name" in person1);//true

  能夠看到,確實,不管屬性在實例對象自己仍是在實例對象的原型對象都會返回true。

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

                function Person(){}
		function hasPrototypeProperty(Object,name){
        	return !Object.hasOwnProperty(name)&&(name in Object);
                }
		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(hasPrototypeProperty(person1,"name"));//true
		person1.name="htt";
		console.log(person1.name);//htt
		console.log(hasPrototypeProperty(person1,"name"));//true
	        delete person1.name;
                console.log(person1.name);//zzw
		console.log(hasPrototypeProperty(person1,"name"));//true

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

 

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

  在經過for-in循環時,它返回的是全部可以經過對象訪問的、可枚舉的屬性,其中既包括存在於實例中的屬性,也包括存在於原型中的屬性。且對於屏蔽了原型中不可枚舉的屬性(即將[[Enumerable]]標記爲false的屬性)也會在for-in中循環中返回。(注:IE早期版本中存在一個bug,即屏蔽不可枚舉屬性的實例屬性不會出如今for-in循環中,這裏不作詳細介紹)

  

		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 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()方法接收一個參數,這個參數能夠是原型對象,也能夠是由構造函數建立的實例對象,返回一個包含全部可枚舉屬性的字符串數組。以下:

	        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";
		console.log(Object.keys(Person.prototype));//["name", "age", "school", "sayName"]
		console.log(Object.keys(person1));//["name", "age"]
		console.log(Object.keys(person2));//[]

  

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

E 更簡單的原型語法

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

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

                function Person(){}
		Person.prototype={
			name:"zzw",
			age:21,
			school:"xjtu",
			sayName:function (){
				console.log(this.name);
			}
		};//原來利用Person.prototype.name="zzw"知識對象中的屬性,對於對象並無任何影響,而這裏建立了新的對象
          

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

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

      咱們能夠經過下面幾句代碼來驗證:

 

                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構造函數。可是這並被影響咱們正常使用,下面幾行代碼即可以清楚地看出:

	        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構造函數的新原型對象。

    

          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的值真的很是重要的時候,咱們能夠像下面的代碼這樣從新設置會適當的值:

		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()方法。

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

  因而,實際上咱們是能夠經過原生對象的原型來修改它。好比:   

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

   這是,便在窗口中彈出了「This is a string」。儘管能夠這樣作,可是咱們不推薦在產品化的程序中修改原生對象的原型。這樣作有可能致使命名衝突等問題。

G.原型模式存在的問題

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

  可是這種方法還因爲自己產生了額外的問題。看下面的例子:

               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,這就不是咱們所但願的了。這也是對於包含引用類型的屬性的最大問題。

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

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

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

		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,面向對象)語言經驗的開發人員看到這種模式會以爲奇怪,所以咱們能夠將全部信息都封裝在構造函數中。本質上是經過檢測某個應該存在的方法是否存在或有效,來決定是否要初始化原型。以下例所示:

  

	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 寄生構造函數模式

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

		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中提到過,這種方式可能會致使後續的命名衝突等一系列問題,咱們是不推薦的。而寄生構造函數就能很好的解決這一問題。以下所示:

  

                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

  或者以下所示:

		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的對象。穩妥對象適合在安全的環境中使用,或者在防止數據被其餘應用程序改動時使用。舉例以下:

	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()方法外沒有別的方式能夠訪問其數據成員。例如在上述代碼下添加:

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

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

 

 

 

第七部分:總結

  在這篇博文中,在建立大量類似對象的前提下,我以分析各類方法利弊的思路下向你們按部就班地介紹了Object構造函數和對象字面量方法、工廠模式、自定義構造函數模式、原型模式、組合使用自定義構造函數模式和原型模式、動態原型模式、寄生構造函數模式、穩妥構造函數模式這幾種模式,其中我認爲組合使用自定義構造函數模式和原型模式以及動態原型模式都是很是不錯的模式。而對於建立對象數量很少的狀況下,對象字面量方法、自定義構造函數模式也都是不錯的選擇。

  這一部份內容屬於JavaScript中的重難點,但願你們多讀幾遍,相信必定會有很大的收穫! 

  在寫這篇博文的過程當中,設及知識點較多,錯誤在所不免,但願你們批評指正。

 

 

命是弱者的藉口,運是強者的謙辭。

 

 

 點擊這裏,回到頁首。

相關文章
相關標籤/搜索