1. 程式人生 > >前端開發:JavaScript中建立物件的9種方式

前端開發:JavaScript中建立物件的9種方式

       ECMA-262把物件定義為:”無需屬性的集合,其屬性可以包含基本值、物件或者函式。”嚴格來講,這就相當於說明物件是一組沒有特定順序的值。物件的每個屬性或方法都有一個名字,而每個名字都對映到一個值。正因為這樣,我們可以把ECMAScript的物件想象成散列表:無非就是一組名對值,其中值可以是資料或函式。 
       建立自定義物件最簡單的方式就是建立一個Object的例項,然後再為他新增屬性和方法,如下所示:

var person = new Object();
person.name = "liubei";
person.age = 29;
person.job = "shayemuyou"
; person.sayName = function(){ alert(this.name); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

       上面的例子建立了一個名為person的物件,併為他添加了三個屬性和一個方法。其中sayName()方法用於顯示name屬性,this.name將被解析為person.name,早期的開發人員經常使用這個模式來建立物件,後來物件字面量的方法成了建立物件的首選模式,上面的例子用物件字面量的語法可以寫成如下這樣:

var person = {
    name:"liubei",
    age:29,
    job:"shayemuyou"
, sayName:function(){ alert(this.name); } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

       這個例子中的person物件和前面的物件是一樣的,都有相同的屬性和方法。 
       雖然Object建構函式或者物件字面量的方法都可以用來建立單個物件,但是這些方法有個明顯的缺點:使用同一個介面建立很多物件,會產生大量的重複程式碼。為了解決這個方法,人們開始使用工廠模式的一種變體。

一、工廠模式

       工廠模式是軟體工程領域一種廣為人知的設計模式,這種模式抽象了建立具體物件的過程。考慮到ECMAScript中無法建立類,開發人員就發明了一種函式,用函式來封裝以特定介面建立物件的細節,如下所示:

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("wei",25,"software");
var person2 = createPerson("bu",25,"software");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

       函式createPerson()能夠根據接受的引數來構建一個包含所有必要資訊的Person物件。可以多次呼叫這個函式,每次都會返回一個包含三個屬性一個方法的物件。工廠模式雖然解決了建立多個相似物件的問題,但卻沒有解決物件識別的問題,即怎麼樣知道這是哪個物件型別。

二、建構函式模式

       像Array、Object這樣的原生建構函式,在執行時會自動出現在執行環境中。此外,我們可以建立自定義個建構函式,從而定義自定義型別的屬性和方法。例如,我們可以使用建構函式重寫上個例子:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        alert(this.name);
    }
}
var person1 = new Person("wei",25,"software");
var person2 = new Person("bu",25,"software");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

       在這個例子中,Person()函式取代了createPerson()函式,我們注意到Person()createPerson()的不同之處在於:

  • 沒有顯式的建立物件
  • 直接將屬性和方法賦值給this物件
  • 沒有return語句 
           此外,還應該注意到函式名Person使用的是大寫字母P。按照慣例,建構函式始終都應該以一個大寫字母開頭,而非建構函式則應該以一個小寫字母開頭。這個做法借鑑了其他OO語言,主要是為了區別於ECMAScript中的其他函式。因為建構函式本身也是函式,只不過可以建立物件而已。 
           要建立一個Person例項,必須使用new操作符。以上這種方式會經過以下四個步驟: 
        1.建立一個新物件 
        2.將建構函式的作用域賦給新物件(因此this指向這個新物件) 
        3.執行建構函式中的程式碼 
        4.返回新物件 
           在前面例子的最後,person1person2分別儲存著Person的一個不同的例項。這兩個物件都有一個constructor(建構函式)屬性,該屬性指向Person。如下:
console.log(person1.constructor == Person);     //true
console.log(person2.constructor == Person);     //true
  • 1
  • 2
  • 1
  • 2

       物件的constructor屬性最初是用來標識物件型別的。但是,提到檢測物件型別,還是instanceof操作符比較可靠一些。我們在這個例子中建立的物件都是Object物件的例項,也是Person物件的例項,這一點通過instanceof操作符可以驗證。

console.log(person1 instanceof Object);     //true
console.log(person1 instanceof Person);     //true
console.log(person2 instanceof Object);     //true
console.log(person2 instanceof Person);     //true
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

       建立自定義的建構函式意味著將來可以將他的例項標識為一種特定的型別;而這正是建構函式模式勝過工廠模式的地方。在這個例子中,person1person2之所以同是Object的例項,是因為所有的物件都繼承自Object。 
       建構函式的主要問題,就是每個方法都要在例項上重新建立一遍,造成記憶體浪費。在前面的例子中,person1person2都有一個名為sayName()的方法,但是兩個方法不是同一Function的例項。不要忘了ECMAScript中的函式也是物件,因此每定義一個函式,也就是例項化了一個物件,從邏輯角度講,此時的建構函式可以這樣定義:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("alert(this.name);")   //與宣告函式在邏輯上是等價的
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

       從這個角度來看建構函式,更容易看明白每個Person例項都會包含一個不同的Function例項的本質。說明白些,會導致不同的作用域鏈和識別符號解析,但是建立Function新例項的機制仍然是相同的。因此,不同例項上的同名函式是不相等的,以下程式碼可以證實這一點。

alert(person1.sayName == person2.sayName);  //false
  • 1
  • 1

       然而,建立兩個完成同樣任務的Function例項的確沒有必要;況且有this物件在,根本不用在執行程式碼前就把函式繫結到特定的物件上。因此,可以像下面這樣,通過把函式定義轉移到建構函式外部來解決這個問題。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}
function sayName(){
    alert(this.name);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

       這樣做解決了多個函式解決相同問題的問題,但是有產生了新的問題,在全域性作用域中實際上只被某個物件呼叫,這讓全域性物件有點名不副實。更讓人無法接受的是:如果物件需要定義很多方法,那麼就要定義很多全域性函式,於是我們這個自定義的引用型別就絲毫沒有封裝性可言了。好在這些問題可以使用原型模式來解決。

三、原型模式

       我們建立的每個函式都有一個prototype(原型)屬性,這個屬性是一個指標,指向一個物件,而這個物件的用途是包含可以由特定型別的所有例項共享的屬性和方法。使用原型物件的例項就是讓所有例項共享它所包含的屬性和方法。換句話說,不必在建構函式中定義物件的例項資訊,而是可以將這些資訊直接新增到原型物件中,如下所示:

function Person(){
}
Person.prototype.name = "wei";
Person.prototype.age = 27;
Person.prototype.job = "Software";
Person.prototype.sayName = function(){
    alert(this.name);
}

var person1 = new Person();
person1.sayName();      //"wei"

var person2 = new Person();
person2.sayName();      //"wei"

alert(person1.sayName == person2.sayName);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

       在此,我們將sayName()方法和所有的屬性直接新增在了Personprototype屬性中,建構函式變成了空函式。即便如此,我們仍然可以通過建構函式來建立新物件,而且新物件還會具有相同的屬性和方法。但是與建構函式不同的是,新物件的這些屬性和方法是由所有例項共享的。換句話說,person1person2訪問的都是同一組屬性和同一個sayName()函式。要理解原型模式的工作原理,就必須先理解ECMAScript中原型物件的性質。 
       原型物件的本性由於篇幅太長將會在下一章節詳細分析。上面我們說了原型模式的好處,接下來我們來看一下原型模式的缺點。原型模式省略了為建構函式傳遞引數的這一環節,結果所有例項在預設情況下都具有相同的屬性值。這會在某些程度上帶來一種不便,這並不是原型模式最大的問題,因為如果我們想為一個通過原型模式建立的物件新增屬性時,新增的這個屬性就會遮蔽原型物件的儲存的同名屬性。換句話說,就是新增的這個屬性會阻止我們去訪問原型中的屬性,但並不會改變原型中的屬性。 
       原型模式最大的問題是由其共享的本質所導致的。原型中所有的屬性被很多例項共享,這種共享對函式非常合適,對包含基本值的屬性也說的過去,但是對引用型別的屬性值來說問題就比較突出了,下面我們來看一個例子:

function Person(){
}
Person.prototype = {
    constructor:Person,
    name:"wei",
    age:29,
    friends:["乾隆","康熙"],
    sayName:function(){
        alert(this.name);
    }
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push("嬴政");
console.log(person1.friends);   //["乾隆","康熙","嬴政"]
console.log(person2.friends);   //["乾隆","康熙","嬴政"]
console.log(person1.friends === person2.friends);   //true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

       上面的例子中,Person.prototype物件有一個名為friends的屬性,該屬性包含一個字串陣列。然後建立了兩個Person的例項,接著修改person1.friends引用的陣列,向陣列中新增一個字串,由於陣列存在於Person.prototype中而不是person1中,所以person2.friends也會被修改。但是一般每個物件都是要有屬於自己的屬性的,所以我們很少看到有人單獨使用原型模式來建立物件。

四、組合使用建構函式模式和原型模式

       建立自定義型別最常見的方式就是組合使用建構函式模式與原型模式。建構函式模式用於定義例項屬性,原型模式用於定義方法和共享的屬性。結果,每個例項都會有自己的一份例項屬性的副本,但同時又共享著對方法的引用,最大限度的節省了記憶體。另外,這種混成模式還支援向建構函式傳遞引數;可謂是集兩種模式之長。下面的程式碼重寫了前面的例子:

function Person(name, age){
    this.name = name;
    this.age = age;
    this.friends = ["乾隆","康熙"];
}
Person.prototype = {
    constructor:Person,
    sayName:function(){
        alert(this.name);
    }
}
var person1 = new Person("wei",29);
var person2 = new Person("bu",25);
person1.friends.push("嬴政");
console.log(person1.friends);   //["乾隆", "康熙", "嬴政"]
console.log(person2.friends);   //["乾隆", "康熙"]
console.log(person1.friends === person2.friends);   //false
console.log(person1.sayName === person2.sayName);   //true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

       在這個例子中,例項屬性都是在建構函式中定義的,而由所有例項共享的屬性constructor和方法sayName()則是在原型中定義的。所以修改了person1.friends並不會改變person2.friends,因為他們分別引用了不同的陣列。 
       這種建構函式與原型模式混成的模式,是目前在ECMAScript中使用最廣泛、認同度最高的一種建立自定義型別的方法。可以說,這是用來定義引用的一種預設形式。

五、動態原型模式

       有其他OO語言經驗的開發人員在看到獨立的建構函式和原型時,很可能會感到非常的困惑。動態原型模式就是用來解決這個問題的一個方案,它把所有的資訊都封裝在了建構函式中,而通過建構函式中初始化原型(僅在必要的情況下),又保持了同時使用建構函式和原型的優點。換句話說,可以通過檢查某個應該存在的方法是否有效,來決定是否要初始化原型。來看一個例子:

function Person(name, age){
    this.name = name;
    this.age = age;
    this.friends = ["乾隆","康熙"];
    //注意if語句
    if(typeof this.sayName!="function"){
        Person.prototype.sayName = function(){
            alert(this.name);
        }
    }
}
var person1 = new Person("wei",29);
person1.friends.push("嬴政");
person1.sayName();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

       注意建構函式程式碼中的if語句,這裡只在sayName()方法不存在的情況下才會將它新增到原型中。這斷程式碼只有在第一次呼叫建構函式的時候才會被執行。此後,原型已經被初始化,不需要再做什麼修改。不過要記住,這裡所做的修改能立即在所有例項中得到反映。因此,這種方法可以說確實非常完美。其中if語句檢查的是初始化之後應該存在的任何方法和屬性–不必再用一大堆if來檢查每個屬性和方法,只檢查其中一個即可。對於採用這樣模式建立的物件,還可以使用instanceof操作符來確定他的型別。 
       注意:使用動態原型模式時,不能使用物件字面量重寫原型。如果在已經建立了例項的情況下重寫原型,那麼就會切斷現有的例項與新原型之間的聯絡。

六、寄生建構函式模式

       通常,在上述幾種模式都不適合的情況下可以使用寄生建構函式模式。這種模式的基本思想是建立一個函式,該函式的作用僅僅是封裝建立物件的程式碼,然後再返回新建立的物件,但從表面看,這個函式又很像典型的建構函式。來看一個例子:

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 person = new Person("wei",29,"banzhuan");
person.sayName();   //"wei"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

       在這個例子中,Person函式建立了一個物件,並以相應的屬性和方法初始化該物件,然後返回了這個物件。除了使用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");
console.log(colors.toPipedString());    //red|blue|green
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

       在這個例子中,我們建立了一個名為SpecialArray的建構函式。在這個函式的內部,首先建立了一個數組,然後push()方法初始化了陣列的值。隨後又給陣列例項添加了toPipedString()方法,用來返回以豎線分隔的陣列值。最後將陣列以函式的形式返回。接著,我們呼叫了SpecialArray建構函式,傳入了初始化的值,並呼叫了toPipedString()方法。 
       關於寄生建構函式模式,有一點需要宣告:首先,返回的物件與建構函式或者建構函式的原型沒有任何關係;也就是說,建構函式返回的物件與在建構函式外部建立的物件沒有什麼不同。為此,不能依賴instanceof操作符來確定物件的型別。由於存在這一的問題,我們建議在可以使用其他模式的情況下不要使用這種模式。

七、穩妥建構函式模式

       道格拉斯·克拉克福德發明了JavaScript中的穩妥物件這個概念。所謂穩妥物件,是指沒有公共屬性,而且其方法也不引用this物件。穩妥物件最適合在一些安全環境中(這些環境會禁止使用thisnew),或者在防止資料被其他應用程式改動時使用。穩妥建構函式遵循的與寄生建構函式類似的模式,但又兩點不同:一是新建立物件的例項方法不引用this;二是不使用new操作符呼叫建構函式。按照穩妥建構函式的要求,可以將前面的Person建構函式重寫如下:

function Person(name, age, job){
    //建立要返回的新物件
    var o = new Object();

    //可以在這裡定義私有變數和函式

    //新增方法
    o.sayName = function(){
        alert(this.name);
    };

    //返回物件
    return o;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

       注意,在以這種模式建立的物件中,除了使用sayName()方法之外,沒有其他辦法訪問name的值。可以像下面使用穩妥的Person建構函式:

var person =Person("weiqi",22,"banzhuan");
person.sayName();   //weiqi
  • 1
  • 2
  • 1
  • 2

       這樣,變數person中儲存的是一個穩妥物件,而除了sayName()方法外,沒有別的方式可以訪問其他資料成員。即使有其他程式碼會給這個物件新增方法或資料成員,但也不可能有別的辦法訪問傳入到建構函式中的原始資料。穩妥建構函式模式提供的這種安全性,使得他非常適合在某些安全執行環境–例如,ADsafe(www.adsafe.org)提供的環境下使用。 
       注意:與寄生建構函式模式類似,使用穩妥建構函式模式建立的物件與建構函式之間沒有什麼關係,因此instanceof操作符對這種物件也沒有意義。