ECMA-262把物件定義為:」無需屬性的集合,其屬性可以包含基本值、物件或函數。」嚴格來講,這就相當於說明物件是一組沒有特定順序的值。物件的每個屬性或方法都有一個名字,而每個名字都映射到一個值。正因為這樣,我們可以把ECMAScript的物件想像成散列表:無非就是一組名對值,其中值可以是資料或函數。
創建自訂物件最簡單的方式就是建立一個Object的實例,然後再為他添加屬性和方法,如下所示:
var person = new Object(); person.name = "liubei"; person.age = 29; person.job = "shayemuyou"; person.sayName = function(){ alert(this.name); }
上面的例子創建了一個名為person的對象,並為他添加了三個屬性和一個方法。其中sayName()方法用於顯示name屬性,this.name將被解析為person.name,早期的開發人員經常使用這個模式來創建對象,後來對象字面量的方法成了創建對象的首選模式,上面的範例用物件字面量的語法可以寫成如下:
var person = { name:"liubei", age:29, job:"shayemuyou", sayName:function(){ alert(this.name); }}
這個範例中的person物件和前面的物件是一樣的,都有相同的屬性和方法。
雖然Object構造函數或物件字面量的方法都可以用來創建單一對象,但是這些方法有個明顯的缺點:使用同一個介面創建很多對象,會產生大量的重複程式碼。為了解決這個方法,人們開始使用工廠模式的變體。
1 工廠模式
工廠模式是軟體工程領域一種廣為人知的設計模式,這種模式抽象化了創造特定物件的過程。考慮到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");
函數createPerson()能夠根據接受的參數來構建一個包含所有必要資訊的Person對象。可以多次呼叫這個函數,每次都會傳回一個包含三個屬性一個方法的物件。工廠模式雖然解決了創建多個相似物件的問題,但卻沒有解決物件辨識的問題,也就是怎麼樣知道這是哪個物件類型。
2 建構子模式
像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");
在這個範例中,Person()函數取代了createPerson()函數,我們注意到Person()與createPerson()的不同之處在於:
沒有明確的創建對象
直接將屬性和方法賦值給this對象
沒有return語句
依照慣例,建構子總是應該以一個大寫字母開頭,而非建構子則應該以一個小寫字母開頭。這個做法借鏡了其他OO語言,主要是為了區別於ECMAScript中的其他函數。因為建構函數本身也是函數,只不過可以創建物件而已。
要建立一個Person實例,必須使用new運算元。以上這個方法會經過以下四個步驟:
1.建立一個新物件
2.將建構子的作用域賦給新物件(因此this指向這個新物件)
3.執行建構子中的代碼
4.返回新物件
在前面範例的最後,person1和person2分別保存Person的一個不同的實例。這兩個物件都有一個constructor(建構函式)屬性,該屬性指向Person。如下:
console.log(person1.constructor == Person); //true console.log(person2.constructor == Person); //true console.log(person1.constructor == Person); //true console.log(person2.constructor == Person); //true
物件的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
創建自訂的建構函式意味著將來可以將他的實例標識為一種特定的型別;而這正是建構函式模式勝過工廠模式的地方。在這個例子中,person1和person2之所以同是Object的實例,是因為所有的物件都繼承自Object。
建構子的主要問題,就是每個方法都要在實例上重新創建一遍,造成記憶體浪費。在前面的範例中,person1和person2都有一個名為sayName()的方法,但兩個方法不是同一Function的實例。不要忘了ECMAScript中的函數也是對象,因此每定義一個函數,也就是實例化了一個對象,從邏輯角度講,此時的構造函數可以這樣定義:
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = new Function("alert(this.name);") //与声明函数在逻辑上是等价的}
从这个角度来看构造函数,更容易看明白每个Person实例都会包含一个不同的Function实例的本质。说明白些,会导致不同的作用域链和标识符解析,但是创建Function新实例的机制仍然是相同的。因此,不同实例上的同名函数是不相等的,以下代码可以证实这一点。
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);
这样做解决了多个函数解决相同问题的问题,但是有产生了新的问题,在全局作用域中实际上只被某个对象调用,这让全局对象有点名不副实。更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在这些问题可以使用原型模式来解决。
3 原型模式
我们创建的每个函数都有一个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);
在此,我们将sayName()方法和所有的属性直接添加在了Person的prototype属性中,构造函数变成了空函数。即便如此,我们仍然可以通过构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但是与构造函数不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1和person2访问的都是同一组属性和同一个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
上面的例子中,Person.prototype对象有一个名为friends的属性,该属性包含一个字符串数组。然后创建了两个Person的实例,接着修改person1.friends引用的数组,向数组中添加一个字符串,由于数组存在于Person.prototype中而不是person1中,所以person2.friends也会被修改。但是一般每个对象都是要有属于自己的属性的,所以我们很少看到有人单独使用原型模式来创建对象。
4 组合使用构造函数模式和原型模式
创建自定义类型最常见的方式就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度的节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。下面的代码重写了前面的例子:
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
在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性constructor和方法sayName()则是在原型中定义的。所以修改了person1.friends并不会改变person2.friends,因为他们分别引用了不同的数组。
这种构造函数与原型模式混成的模式,是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用的一种默认形式。
5 动态原型模式
有其他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();
注意构造函数代码中的if语句,这里只在sayName()方法不存在的情况下才会将它添加到原型中。这断代码只有在第一次调用构造函数的时候才会被执行。此后,原型已经被初始化,不需要再做什么修改。不过要记住,这里所做的修改能立即在所有实例中得到反映。因此,这种方法可以说确实非常完美。其中if语句检查的是初始化之后应该存在的任何方法和属性–不必再用一大堆if来检查每个属性和方法,只检查其中一个即可。对于采用这样模式创建的对象,还可以使用instanceof操作符来确定他的类型。
注意:使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有的实例与新原型之间的联系。
6 寄生构造函数模式
通常,在上述几种模式都不适合的情况下可以使用寄生构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象,但从表面看,这个函数又很像典型的构造函数。来看一个例子:
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"
在这个例子中,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
在这个例子中,我们创建了一个名为SpecialArray的构造函数。在这个函数的内部,首先创建了一个数组,然后push()方法初始化了数组的值。随后又给数组实例添加了toPipedString()方法,用来返回以竖线分隔的数组值。最后将数组以函数的形式返回。接着,我们调用了SpecialArray构造函数,传入了初始化的值,并调用了toPipedString()方法。
关于寄生构造函数模式,有一点需要声明:首先,返回的对象与构造函数或者构造函数的原型没有任何关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖instanceof操作符来确定对象的类型。由于存在这一的问题,我们建议在可以使用其他模式的情况下不要使用这种模式。
7 稳妥构造函数模式
道格拉斯·克拉克福德发明了JavaScript中的稳妥对象这个概念。所谓稳妥对象,是指没有公共属性,而且其方法也不引用this对象。稳妥对象最适合在一些安全环境中(这些环境会禁止使用this和new),或者在防止数据被其他应用程序改动时使用。稳妥构造函数遵循的与寄生构造函数类似的模式,但又两点不同:一是新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的Person构造函数重写如下:
function Person(name, age, job){ //创建要返回的新对象 var o = new Object(); //可以在这里定义私有变量和函数 //添加方法 o.sayName = function(){ alert(this.name); }; //返回对象 return o; }
注意,在以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问name的值。可以像下面使用稳妥的Person构造函数:
var person =Person("weiqi",22,"banzhuan"); person.sayName(); //weiqi
这样,变量person中保存的是一个稳妥对象,而除了sayName()方法外,没有别的方式可以访问其他数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。稳妥构造函数模式提供的这种安全性,使得他非常适合在某些安全执行环境–例如,ADsafe(www.adsafe.org)提供的环境下使用。
注意:與寄生建構函式模式類似,使用穩健建構函式模式建立的物件與建構函式之間沒有什麼關係,因此instanceof操作子對這種物件也沒有意義。
以上就是JavaScript中創建物件的7種模式的內容,更多相關內容請關注PHP中文網(m.sbmmt.com)!