標籤:建立對象 soft join text 等於 block 寫法 instance lob
雖然Object建構函式或者對象字面量都可以用來建立單個對象,但這些方式有個明顯的缺點:使用同一個介面建立很多個物件會產生大量的重複代碼。為瞭解決這個問題,人們開始使用原廠模式的一種變體。
1、原廠模式
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 person1 = createPerson(‘zcy‘,26,‘Software Engineer‘);var person2 = createPerson(‘bb‘,27,‘Software Engineer‘);
函數createPerson()能夠根據接受的參數來構建一個包含所有必要資訊的person對象。可以無數次地調用這個函數,而每次它都會返回一個包含三個屬性一個方法的對象。原廠模式雖然解決了建立多個相似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型),隨著javascript的發展,又一個新模式出現了。
2、建構函式模式
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = function(){ alert(this.name); };} var person1 = new Person(‘zcy‘,26,‘Software Engineer‘);var person2 = new Person(‘bb‘,27,‘Software Engineer‘);
建構函式模式與原廠模式有以下不同之處:
沒有顯式地建立對象;
直接將屬性和方法賦給了this對象;
沒有return語句。
要建立Person的新執行個體,必須使用new操作符。這種方式調用建構函式實際上會經曆以下4個步驟:
(1)建立一個新對象;
(2)將建構函式的範圍賦給新對象(因此this就指向了這個新對象);
(3)執行建構函式中的代碼(為這個新對象添加屬性);
(4)返回新對象。
前面的例子中,person1和person2分別儲存著Person的一個不同的執行個體。這兩個對象都有一個constructor(建構函式)屬性,該屬性指向Person。
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
檢測物件類型,可以使用instanceof操作符,這個例子中國年建立的所有對象即是Object的執行個體,同時也是Person的執行個體。
建立自訂的建構函式意味著將來可以將它的執行個體標識為一種特定的類型;而正是建構函式模式勝過原廠模式的地方。
建構函式與其它函數的唯一區別,就在於調用它們的方式不同。不過建構函式畢竟也是函數,不存在定義建構函式的特殊文法,任何函數只要通過new操作符來調用,那它就可以作為建構函式;而任何函數,如果不通過new操作符來調用,那它跟普通函數也不會有什麼兩樣。前面定義的Person()函數可以通過下列任何一種方式來調用。
//當作建構函式使用var person = new Person(‘zcy‘,26,‘Software Engineer‘);person.sayName();//"zcy"//當作普通函數調用
//當在全域範圍中調用一個函數時,this對象總是指向Global對象(在瀏覽器中是window對象)Person(‘zcy‘,26,‘Software Engineer‘);window.sayName();//"zcy"//在另一個對象的範圍中調用var o= new Object();Person.call(o,‘zcy‘,26,‘Software Engineer‘);o.sayName();//"zcy"
使用建構函式的主要問題,就是每個方法都要在每個執行個體上重新建立一遍,在前面的例子中,person1和person2都有一個名為sayName()的方法,但那兩個方法不是同一個function的執行個體。不要忘了ECMAScript中的函數是對象,因此每定義一個函數,也就是執行個體化了一個對象。
alert(person1.sayName == person2.sayName); //false
建立兩個完成同樣任務的function執行個體的確沒有必要,況且有this對象在,根本不用在執行代碼前久把函數綁定到特定對象上面。因此可以如下把函數定義轉移到建構函式外面來解決這個問題。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = sayName;} function sayName(){ alert(this.name);}var person1 = new Person(‘zcy‘,26,‘Software Engineer‘);var person2 = new Person(‘bb‘,27,‘Software Engineer‘);
這樣做確實解決了兩個函數做同一件事的問題,可是新問題又來了:在全域範圍中定義的函數實際上只能被某個對象調用,這讓全域範圍有點名不副實。而更讓人無法接受的是:如果對象需要定義很多方法,那麼就要定義很多個全域函數,於是我們這個自訂的參考型別就絲毫沒有封裝性可言了。好在這些問題可以通過使用原型模式來解決。
3、原型模式
我們建立的每一個函數都有一個prototype(原型)屬性,這個屬性是一個指標,指向一個對象,而這個對象的用途是包含可以由特定類型的所有執行個體共用的屬性和方法。如果按照字面意思來理解,那麼prototype就是通過調用建構函式而建立的那個對象執行個體的原型對象。使用原型對象的好處是可以讓所有對象執行個體共用它所包含的屬性和方法。換句話說,不必在建構函式中定義對象執行個體的資訊,而是可以將這些資訊直接添加到原型對象中。如下所示。(個人理解,原型中的屬性和方法相當於java類中的static屬性和方法)
function Person(){}Person.prototype.name = ‘zcy‘;Person.prototype.age = 26;Person.prototype.job = ‘Software Engineer‘;Person.prototype.sayName = function(){ alert(this.name); }var person1 = new Person();person1.sayName(); //‘zcy‘var person2 = new Person();person2.sayName(); //‘zcy‘alert(person1.sayName == person2.sayName);//true
無論什麼時候,只要建立了一個新函數,就會根據一組特定的規則為該函數建立一個prototype屬性,這個屬性指向函數的原型對象。在預設情況下,所有的原型對象都會自動獲得一個constructor(建構函式)屬性,這個屬性是一個指向prototype屬性所在函數的指標。如前面的例子,Person.prototype.constructor指向Person。而通過這個建構函式,我們還可以繼續為原型對象添加其他屬性和方法。
建立了自訂的建構函式之後,其原型對象預設只會取得constructor屬性;至於其他方法,則都是從Object 繼承而來的。當調用建構函式建立一個新執行個體後,該執行個體的內部將包含一個指標(內部屬性),指向建構函式的原型對象。ECMA-262第5版中管這個指標叫[[Prototype]]。要明確的一點是,這個串連存在於執行個體與建構函式的原型對象之間,而不是存在於執行個體與建構函式之間。
每當代碼讀取某個對象的某個屬性時,都會執行一次搜尋,目標是具有給定名字的屬性。搜尋首先從對象執行個體本身開始。如果在執行個體中找到了具有給定名字的屬性,則返回該屬性;如果沒有找到,則繼續搜尋指標指向的原型對象,在原型對象中尋找具有給定名字的屬性,如果在原型對象中找到了這個屬性,則返回該屬性的值。
雖然可以通過對象執行個體訪問儲存在原型中的值,但卻不能通過對象執行個體重寫原型中的值。如果我們再執行個體中添加了一個屬性,而該屬性與執行個體原型中的一個屬性同名,那我們就在執行個體中建立該屬性,該屬性將會屏蔽原型中的那個屬性。執行個體中添加與原型同名的屬性只會阻止我們訪問原型中的那個屬性,但不會修改那個屬性。即使將這個屬性設定為null,也只會在執行個體中設定這個屬性,而不會恢複其指向原型的串連。不過使用delete操作符則可以完全刪除執行個體屬性,從而讓我們能夠重新訪問原型中的屬性。
使用hasOwnProperty()方法可以檢測一個屬性是否存在於執行個體中還是存在於原型中,這個方法只在給定屬性存在於對象執行個體中時,才會返回true。
另外,有兩種方式使用in操作符,單獨使用和在for-in迴圈中使用。在單獨使用時,in操作符會在通過對象能夠訪問給定屬性時返回true,無論該屬性是存在於執行個體還是原型中。所以同時使用hasOwnProperty()方法和in操作符,就可以確定該屬性到底是存在於對象中,還是存在於原型中。
在使用for-in迴圈時,返回的是所有能夠通過對象訪問的,可枚舉的(enumerated)屬性,其中既包括存在於執行個體中的屬性,也包括存在於原型中的屬性。屏蔽了原型中不可枚舉屬性(即將[[Enumerable]]標記為false的屬性)的執行個體屬性也會在for-in迴圈中返回,因為根據規定,所有開發人員定義的屬性都是可枚舉的-----只有在IE8及更早版本中例外。
IE早期版本的實現中存在一個bug,即屏蔽不可枚舉屬性的執行個體屬性不會出現在for-in迴圈中。
前面的原型例子更簡單的寫法是這種
function Person(){}Person.prototype = { name:‘zcy‘, age:26, job:‘Software Engineer‘, sayName:function(){ alert(this.name); } };
此種寫法與前面的不同之處是:constructor屬性不再指向Person了。因為每建立一個函數,就會同時建立它的prototype對象,這個對象也會自動獲得constructor屬性。而我們在這裡使用的文法,本質上完全重寫了預設的prototype對象,因此constructor屬性也就變成了新對象的constructor屬性(指向Object建構函式),不再指向Person函數。如果constructor的值真的很重要,可以像下面這樣特意將它設定回適當的值。
function Person(){}Person.prototype = { constructor:Person, name:‘zcy‘, age:26, job:‘Software Engineer‘, sayName:function(){ alert(this.name); } };
注意,以這種方式重設constructor屬性會導致它的[[Enumerable]]特性被設定為true。預設情況下原生的constructor屬性是不可枚舉的。
執行個體與原型之間存在鬆散串連關係,對原型對象的任何修改都能夠立即從執行個體上反映出來,即使是先建立了執行個體後修改原型也是如此。因為執行個體與原型之間的串連是一個指標,而非一個副本。
但是如果是重新寫整個原型對象,那麼情況就會不一樣。我們知道調用一個建構函式是會為執行個體添加一個指向最初原型的[[Prototype]]指標,而把原型修改為另外一個對象就等於切斷了建構函式與最初原型之間的聯絡,請記住:執行個體中的指標僅指向原型,而不指向建構函式。
function Person(){}var friend = new Person();Person.prototype = { name:‘zcy‘, age:26, job:‘Software Engineer‘, sayName:function(){ alert(this.name); } };friend.sayName(); //error
在這個例子中,我們先建立了Person的一個執行個體,然後又重寫了其原型對象,然後在調用friend.sayName()時發生了錯誤,因為friend指向的原型中不包含以該名字命名的屬性。
重寫原型對象之前
重寫原型對象之後
可以看出重寫原型對象切斷了現有原型與任何之前已經存在的對象執行個體之間的聯絡;它們引用的仍然是最初的原型。
原型模式也不是沒有缺點。首先,它省略了為建構函式傳遞初始化參數這一環節,結果所有執行個體在預設情況下都將取得相同的屬性值。雖然這會在某種程度上帶來一些不方便,但還不是原型的最大問題。原型模式的最大問題是由其共用的本性所導致的。
原型中所有屬性是被很多執行個體共用的,這種共用對於函數非常合適,對於那些包含基本值的屬性倒也說的過去,畢竟通過在執行個體上添加同名屬性可以隱藏原型中的對應屬性。然而對於包含參考型別的屬性來說問題就比較突出了。如下例子。
function Person(){}Person.prototype = { constructor:Person, name:‘zcy‘, age:26, job:‘Software Engineer‘, friends:{‘Shelby‘,‘Court‘}, sayName:function(){ alert(this.name); } };var person1 = new Person();var person2 = new Person();person1.friends.push(‘Van‘);alert(person1.friends); //"Shelby,Court,Van"alert(person2.friends); //"Shelby,Court,Van"alert(person1.friends == person2.friends); //true
可見,對原型中的參考型別修改會反映到所有執行個體上來。假如我們的初衷就是像這樣,在所有執行個體中共用一個數組,那麼這個結果沒問題。可是,執行個體一般都是要有屬於自己的全部屬性的,而這個問題正是我們很少看到有人單獨使用原型模式的原因所在。(此處修改原型中的參考型別與前面例子中在執行個體中給與原型中同名的基本類型屬性賦值不同,在執行個體中給基本類型屬性賦值只會屏蔽原型中的同名屬性,並不會修改原型中同名屬性的值)。
4、組合使用建構函式模式和原型模式
建立自訂類型的最常見方式,就是組合使用建構函式模式與原型模式。建構函式模式用於定義執行個體屬性,而原型模式用於定義方法和共用屬性。結果每個執行個體都會有自己的一份執行個體屬性的副本,但同時又共用著對方法的引用,最大限度地節省了記憶體。另外這種混成模式還支援向建構函式傳遞參數,可謂是集兩種模式之長。如下例子。
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.friends = {‘Shelby‘,‘Court‘};}Person.prototype = { constructor:Person, sayName:function(){ alert(this.name); } };var person1 = new Person(‘zcy‘,26,‘Software Engineer‘);var person2 = new Person(‘bb‘,27,‘Doctor‘);person1.friends.push(‘Van‘);
alert(person1.friends); //"Shelby,Court,Van"alert(person2.friends); //"Shelby,Court"alert(person1.friends == person2.friends); //false
alert(person1.sayName == person2.sayName); //true
這種混成模式,是目前在ECMAScript中使用最廣泛的,認可度最高的一種建立自訂類型的方法。可以說,這是用來定義參考型別的一種預設模式。
5、動態原型模式
有其它OO語言經驗的開發人員在看到獨立的建構函式和原型時,很有可能會感到非常困惑。動態原型模式正是致力於解決這個問題的一個方案,它把所有資訊都封裝在了建構函式中,而通過在建構函式中初始化原型(僅在必要情況下),又保持了同時使用建構函式和原型的優點。換句話說,可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。來看一個例子。
function Person(name,age,job){ //屬性 this.name = name; this.age = age; this.job = job; //方法 if(typeof this.sayName != "function"){ Person.prototype.sayName = function(){ alert(this.name); }; }}var friend= new Person(‘zcy‘,26,‘Software Engineer‘);friend.sayName();
建構函式代碼中加粗的部分,只有在sayName()方法不存在的情況下,才會將它添加到原型中。這段代碼只會在初次調用建構函式時才會執行。且這裡對原型所做的修改能夠立即在所有執行個體中得到反映。其中if語句檢查的可以使初始化之後應該存在的任何屬性或方法,不必用一大堆if語句檢查每個屬性和每個方法,只檢查其中一個即可。
使用動態原型模式時,不能使用對象字面量重寫原型,因為如果在已經建立了執行個體的情況下重寫原型,那麼就會切斷現有執行個體與新原型之間的聯絡。
6、寄生建構函式模式
通常在前述集線模式都不適用的情況下,可以使用寄生(parasitic)建構函式模式。這種模式的基本思想是建立一個函數,該函數的作用僅僅是封裝建立對象的代碼,然後再返回新建立的對象,從表面上看,這個函數很像是典型的建構函式。如下例子。
function Person(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 friend = new Person(‘zcy‘,26,‘Software Engineer‘);friend.sayName(); //"zcy"
除了使用new操作符並把使用的封裝函數叫做建構函式之外,這個模式跟原廠模式其實是一模一樣的。建構函式在不傳回值得情況下,預設會返回新對象執行個體。而通過在建構函式末尾添加一個return語句,可以重寫調用建構函式時返回的值。
這個模式可以在特殊情況下用來為對象建立建構函式。假設我們想建立一個具有額外方法的特殊數組。由於不能直接修改Array建構函式,因此可以使用這個模式。
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");alert(colors.toPipedString()); //"red|blue|green"
關於寄生建構函式模式,有一點需要說明:首先返回的對象與建構函式或者與建構函式的原型屬性之間沒有關係;也就是說,建構函式返回的對象與在建構函式外部建立的對象沒有什麼不同。為此,不能依賴instanceof操作符來確定物件類型。
7、穩妥建構函式模式
穩妥對象(durable objects),指的是沒有公用屬性,而且其方法也不引用this的對象。穩妥對象最適合在一些安全的環境中(這些環境中會禁止使用this和new),或者在防止資料被其他應用程式(如Mashup程式)改動時使用。穩妥建構函式遵循與寄生建構函式類似的模式,但有兩點不同:一是新建立對象的執行個體方法不引用this;二是不適用new操作符調用建構函式。如下例子。
function Person(name,age,job){ //建立要返回的對象 var o = new Object(); //可以在這裡定義私人變數和函數 //添加方法 o.sayName = function(){ alert(name); }; //返回對象 return o;}var friend = Person("zcybb",26,"Software Engineer");friend.sayName(); //"zcybb"
這種模式建立的對象中,變數friend中儲存的是一個穩妥對象,而除了使用sayName()方法之外,沒有其他辦法訪問name的值。即使有其他代碼會給這個對象添加方法或資料成員,但也不可能有別的辦法訪問傳入到建構函式中的未經處理資料。
與寄生建構函式模式類似,使用穩妥建構函式模式建立的對象與建構函式之間也沒有什麼關係,因此instanceof操作符對這種對象也沒有意義。
javascript建立對象的幾種模式