javaScript建立對象
一、對象ECMA-262把對象定義為:無序屬性的集合,其屬性可以包含基本值,對象或者函數。所以js中對象就是一組索引值對。 物件導向的語言中,都是通過類的來建立任意多個具有相同屬性和方法的對象執行個體的。但是js中沒有類的概念,接下來我先通過一個例子來闡述js中沒有“類”的概念蘊含的哲學。這點會讓初學者很困惑,但是也正因為放下了“類”的概念,js對象才有了其他程式設計語言沒有的活力。事實上js中對象的“類”是從無到有,又不斷演化,最終消失於無形之中。 舉例:小蝌蚪找媽媽的故事,小蝌蚪在其自身類型不斷演化的過程中,逐漸層成了和媽媽一樣的“類”。 代碼: <!DOCTYPE html><html><meta charset="utf-8"><head> <title>小蝌蚪找媽媽</title></head><body><script> var life={};//光溜溜的生命 for(life.age=1;life.age<=3;life.age++) { switch (life.age) { case 1: life.body="卵細胞";//增加body屬性 life.say=function(){ console.log(this.age+this.body); };//建立say方法 break; case 2: life.tail="尾巴";//增加tail屬性 life.gill="腮";//增加gail屬性 life.body="蝌蚪"; life.say=function(){ console.log(this.age+this.body+'-'+this.tail+","+this.gill); }; break; case 3: delete life.tail;//刪除tail屬性 delete life.gill;//刪除gill屬性 life.legs="四條腿";//增加legs屬性 life.lung="肺";//增加lung屬性 life.body="青蛙"; life.say=function(){ console.log(this.age+this.body+"-"+this.legs+","+this.lung); }; break; } life.say();//調用say方法,每次邏輯都會發生動態改變 }</script></body></html> (1)js程式一開始產生了一個生命對象life,life誕生時只是個光溜溜的生命對象,沒有任何屬性和方法。 (2)第一次生命進化,life對象有了身體屬性body,並有了一個say方法,看起來是一個“卵細胞”。 (3)第二次生命進化,它又長出了“尾巴”和“腮”,有了tail和gill屬性,顯示它是一個“蝌蚪”。 (4)第三次生命進化,它的tail和gill消失了,但又長出了“四條腿”和“肺”,有了legs和lung屬性,從而最終變成了“青蛙”。 所以說,對象的“類”是從無到有,又不斷演化,最終消失於無形中。 “類”確實可以協助我們對世界分類,但是我們思想不能被“類”束縛,如果生命開始就被規定了固定的“類”,就無法演化,蝌蚪就變不成青蛙。所以js中沒有“類”,類已化為無形與對象融為一體。這樣也更加貼近現實世界,不是嗎? js沒有類,js對象就是一組索引值對,接下來看看js中9種建立對象的方式。 js建立對象的方法的產生是一個迭代的過程,因為已有方法的缺陷催生出新的方法。首先是早期js程式員經常使用也是最簡單的方法——通過Objec建構函式建立對象。 二、通過Object建構函式建立對象代碼: <script> var person=new Object(); person.nam="lxy"; person.age="22"; person.job="Software Engineer"; person.sayName= function () { alert(this.nam); } person.sayName();</script> 優點:簡單 三、通過字面量建立對象代碼: <script> var person={ name:"lxy", age:22, job:"Software Engineer", sayName:function(){ alert(this.name); } }; person.sayName();</script> 要注意一點就是每聲明一個索引值對後面標點是“,”。 對象字面量相對於Object建構函式代碼量少了一點點。但是這2種方法通過一個介面建立很多個物件,會產生大量重複代碼。Don't Repeat Yourself!我們需要對重複的代碼進行抽象。原廠模式就是在這種情況下出現的。 四、原廠模式 通過類來建立多個執行個體必然可以減少代碼重複,但是ECMAScript中無法建立類,所以就用函數來封裝以特定介面建立對象的細節。 代碼: <script> function createPerson(name ,age,job){ var o=new Object(); o.name=name; o.age=age; o.job=job; o.sayName=function(){ alert(this.name); } return o; } var lxy=createPerson("lxy",22,"Software Engineer"); var strangerA=createPerson("strangerA",24,"Doctor"); lxy.sayName(); strangerA.sayName();</script> 原廠模式減少了重複代碼,但是不能夠識別對象,所有執行個體都是object類型的。 這時建構函式模式就出現了。 五、建構函式模式 像Object和Array這樣的原生建構函式,在運行時會自動出現在執行環境中。我們可以建立自訂建構函式,從而建立特定類型的對象。 代碼: <script> function Person(name ,age,job){ this.name=name; this.age=age; this.job=job; this.sayName=function(){ alert(this.name); } } var lxy=new Person("lxy",22,"Software Engineer"); var strangerA=new Person("strangerA",24,"Doctor"); lxy.sayName(); strangerA.sayName();</script> 建構函式中首字母大寫,而非建構函式首字母小寫作為區別。 通過new操作符來建立Person執行個體,這樣建立的執行個體都有一個constractor(建構函式)屬性,該屬性指向Person。 alert(lxy.constructor==Person);//true alert(strangerA.constructor==Person);//truelxy和strangeA是Person的執行個體,同時也是Object的執行個體。因為所有的對象都繼承自Object。 建立自訂建構函式意味著將來可以將它的執行個體標識為一種特定的類型;而這正是建構函式勝過原廠模式的地方。 建構函式也是函數,所以文法上可以像普通函數一樣去用,但是可以用並不代表應該用,還是以建構函式的方式用更合理。 建構函式的問題是,同一建構函式的不同執行個體的相同方法是不一樣的。 alert(lxy.sayName==strangerA.sayName());//false這個問題很好理解,因為js中函數就是對象,每定義一個函數,也就是執行個體化了一個對象。 從代碼的角度可能理解的更深刻: this.sayName=function(){alert(this.name)};與 this.sayName=new Function(alert(this.name));是等價的。 所以使用建構函式建立對象,每個方法在每個執行個體上都要重新實現一遍,一是耗資源,二是建立兩個或者多個完成同樣認為的Function沒有必要,三是有this在,沒必要在代碼執行前就把函數綁定到特定對象上。 所以,有一種方法是說把函數定義轉移到建構函式外部,代碼如下: <script> function Person(name ,age,job){ this.name=name; this.age=age; this.job=job; this.sayName=sayName; } function sayName(){ alert(this.name); } var lxy=new Person("lxy",22,"Software Engineer"); var strangerA=new Person("strangerA",24,"Doctor"); lxy.sayName(); strangerA.sayName();</script> 把sayName()函數的定義轉移到建構函式外部,成為全域的函數,建構函式內部把sayName設定為全域的sayName。這樣sayName是一個指向外部函數的指標,因此lxy和strangeA就共用了 全域的sayName函數。 alert(lxy.sayName==strangerA.sayName);//true但是這會有更糟糕的問題:全域範圍的函數只能被某個對象調用,這名不副實啊,會造成對全域環境的汙染;更糟糕的是建構函式有多少個方法,就要定義多少個全域函數, 那建構函式就絲毫沒有封裝性可言了。 但是這樣的想法是可貴的,為原型模式做了鋪墊,建構函式建立對象問題的解決辦法是原型模式。 六、原型模式 原型模式就是把建構函式中方法拿出來的基礎上,為了避免對全域環境的汙染,再做了一層封裝,但是畢竟是一種新的模式,它封裝的更徹底,而且也不是把所有的函數都封裝,而是恰到好處的把建構函式中公用的方法和屬性進行了封裝。 代碼: <script type="text/javascript">function Person(){ }Person.prototype.name="lxy";Person.prototype.age=22;Person.prototype.job="Software Engineer";Person.prototype.sayName=function(){ alert(this.name);} var lxy=new Person(); lxy.sayName(); var personA=new Person(); personA.sayName(); alert(lxy.sayName()==personA.sayName());//true</script> 使用原型的好處是可以讓所有的執行個體共用它所包含的屬性和方法。完美的解決了建構函式的問題。因為原型是js中的一個核心內容,其資訊量很大,另作介紹。 原型也有它本身的問題,共用的屬性值如果是參考型別,一個執行個體對該屬性的修改會影響到其他執行個體。這正是原型模式很少單獨被使用的原因。 <script type="text/javascript">function Person(){ }Person.prototype.name="lxy";Person.prototype.age=22;Person.prototype.job="Software Engineer";Person.prototype.friends=["firend1","friend2"];Person.prototype.sayName=function(){ alert(this.name);} var lxy=new Person(); var personA=new Person(); alert(lxy.friends);//friend1,friend2 alert(personA.friends);//friend1,friend2 alert(lxy.friends==personA.friends);//true lxy.friends.push("friend3"); alert(lxy.friends);//friend1,friend2,friend3 alert(personA.friends);//friend1,friend2,friend3 alert(lxy.friends==personA.friends);//true</script> 七、建構函式和原型混合模式 建立自訂類型的最常見方式,就是組合使用建構函式模式和原型模式。建構函式模式用於定義執行個體屬性,而原型模式用於定義共用的方法和屬性。結果,每個執行個體都有一份執行個體屬性的副本,同時又共用著對方法的引用,最大限度的節省了記憶體。另外,這種混合模式還支援向建構函式傳遞參數,可謂是集兩種模式之長。 代碼: <script type="text/javascript">function Person(name,age,job){this.name=name;this.age=age;this.job=job;this.friends=["firend1","friend2"];} Person.prototype={ constructor:Person, sayName:function(){ alert(this.name);}} var lxy=new Person("lxy",22,"Software Engineer"); var personA=new Person("personA",25,"Doctor"); alert(lxy.friends);//friend1,friend2 alert(personA.friends);//friend1,friend2 alert(lxy.friends==personA.friends);//false lxy.friends.push("friend3"); alert(lxy.friends);//friend1,friend2,friend3 alert(personA.friends);//friend1,friend2</script> 執行個體屬性在建構函式中定義,而共用屬性constructor和共用方法sayName()在原型中定義。而修改一個執行個體的friends不會影響其他執行個體的friends,因為它們引用不同數組,根本沒關係。 這種建構函式與原型混合模式,是目前使用最廣泛、認同度最高的一種建立自訂類型的方法。可以說,這是用來定義參考型別的一種預設模式。其實原型就是為建構函式服務的,配合它來建立對象,想要只通過原型一勞永逸的建立對象是不可取的,因為它只管建立共用的屬性和方法,剩下的就交給建構函式來完成。 八、動態原型模式 個人覺得建構函式和原型混合模式已經可以完美的完成任務了。但是動態原型模式的提出是因為混合模式中用了建構函式對象居然還沒建立成功,還需要再操作原型,這在其他OO語言開發人員看來很彆扭。所以把所有資訊都封裝到建構函式中,即通過建構函式必要時初始化原型,在建構函式中同時使用了建構函式和原型,這就成了動態原型模式。真正用的時候要通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。 <script type="text/javascript">function Person(name,age,job){this.name=name;this.age=age;this.job=job;this.friends=["firend1","friend2"];if(typeof this.sayName!="function"){ alert("初始化原型");//只執行一次 Person.prototype.sayName=function(){ alert(this.name); }}} var lxy=new Person("lxy",22,"Software Engineer"); lxy.sayName(); var personA=new Person("personA",25,"Doctor"); personA.sayName();</script> 使用動態原型時,不能使用對象字面量重寫原型。如果在已經建立了執行個體的情況下重寫原型,那麼就會切斷現有執行個體與新原型之間的聯絡。 九、寄生的建構函式模式 在前面幾種模式都不適用的情況下,適用寄生(parasitic)建構函式模式。寄生模式其實就是把原廠模式封裝在建構函式模式裡,即建立一個函數,該函數的作用僅僅是封裝建立對象的代碼,然後再返回新建立的對象,從表面看,這個函數又很像典型的建構函式。 代碼: <script type="text/javascript">function Person(name,age,job){ var o=new Object(); o.name=name; o.age=age; o.sayName=function(){ alert(this.name); } return o;}var lxy=new Person("lxy",22,"Software Engineer");lxy.sayName();</script> 除了適用new操作符使得該函數成為建構函式外,這個模式和原廠模式一模一樣。 返回的對象和建構函式或者構造的原型屬性之間沒有任何關係,所以不能用instanceof,這種方式建立的對象和在建構函式外面建立的對象沒什麼兩樣。 十、穩妥的建構函式模式穩妥的建構函式模式用顯式聲明的方法來訪問屬性。 和這種模式相關的有一個概念:穩妥對象。穩妥對象,指沒有公用對象,而且其方法也不引用this的對象。 穩妥對象最適合在一些安全的環境中(這些環境中會禁用this和new),或者防止資料被其他應用程式(如Mashup)改動時使用。 穩妥建構函式遵循與寄生建構函式類似的模式,但有兩點不同:一是新建立對象的執行個體方法不引用this,而是不使用new操作符調用建構函式。 代碼: <script type="text/javascript">function Person(name,age,job){ //建立要返回的對象 var o=new Object(); //可以在這裡定義私人變數和函數 //添加方法 o.sayName=function(){ alert(name); } return o;}var lxy=new Person("lxy",22,"Software Engineer");lxy.sayName();alert(lxy.name);//undefinedalert(lxy.age);//undefined</script> 以這種模式建立的對象中,lxy是一個穩妥對象,除了使用sayName()方法外,沒有其他方法訪問name的值,age,job類似。 即使有其他代碼會給這個對象添加方法或資料成員,但也不可能有別的方法訪問傳入建構函式中的未經處理資料。 與寄生建構函式模式類似,使用穩妥建構函式模式建立的對象與建構函式之間也沒有什麼關係,因此不能用instanceof操作符。