定義多個(gè)屬性
由于為對(duì)象定義多個(gè)屬性的可能性很大,ES5又定義了一個(gè)Object.defineProperties()方法。利用這個(gè)方法可以通過描述符一次定義多個(gè)屬性。這個(gè)方法接收兩個(gè)對(duì)象參數(shù):第一個(gè)對(duì)象是要添加和修改其屬性的對(duì)象,第二個(gè)對(duì)象的屬性與第一個(gè)對(duì)象中要添加或修改的屬性一一對(duì)應(yīng):
var book = {};
Object.defineProperties(book, {
_year: {
value: 2004
},
edition: {
value: 1
},
year: {
get: function () {
return this._year;
},
set: function (newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
以上代碼在book對(duì)象上定義了兩個(gè)數(shù)據(jù)屬性(_year和edition)和一個(gè)訪問器屬性(year)。最終的對(duì)象與上一節(jié)中定義的對(duì)象相同。唯一的區(qū)別是這里的屬性都是在同一時(shí)間創(chuàng)建的。
支持Object.defineProperties()方法的瀏覽器有IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。
讀取屬性的特性
使用ES5的Object.getOwnProrpertyDescriptor()方法,可以取得給定屬性的描述符。這個(gè)方法接收兩個(gè)參數(shù):屬性所在的對(duì)象和要讀取其描述符的屬性名稱。返回值是一個(gè)對(duì)象,如果是訪問器屬性,這個(gè)對(duì)象的屬性有configurable、enumerable、get 和 set;如果是數(shù)據(jù)屬性,這個(gè)對(duì)象的屬性有configurable、enumerable、writable 和value:
var book = {};
Object.defineProperties(book, {
_year: {
value: 2004
},
edition: {
value: 1
},
year: {
get: function () {
return this._year;
},
set: function (newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
alert(descriptor.value); //2004
alert(descriptor.configurable); //false
alert(typeof descriptor.get); //"undefined"
var descriptor = Object.getOwnPropertyDescriptor(book, "year");
alert(descriptor.value); //undefined
alert(descriptor.enumerable); //false
alert(typeof descriptor.get); //"function"
在JavaScript中,可以針對(duì)任何對(duì)象——包括DOM和BOM對(duì)象,使用Object.getOwnProperty-Descriptor()方法。支持這個(gè)方法的瀏覽器有IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。
創(chuàng)建對(duì)象
雖然Object構(gòu)造函數(shù)或?qū)ο笞置媪慷伎梢杂脕韯?chuàng)建單個(gè)對(duì)象,但這些方式有個(gè)明顯的缺點(diǎn):使用同一個(gè)接口創(chuàng)建很多對(duì)象,會(huì)產(chǎn)生大量的重復(fù)代碼。為了解決這個(gè)問題,人們開始使用工廠模式的一種變體。
工廠模式
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("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
工廠模式雖然解決了創(chuàng)建多個(gè)相似對(duì)象的問題,但卻沒有解決對(duì)象識(shí)別的問題(即怎樣知道一個(gè)對(duì)象的類型)。
構(gòu)造函數(shù)模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
在這個(gè)例子中,Person()函數(shù)取代了createPerson()函數(shù)。我們注意到,Person()中的代碼除了與createPerson()中相同的部分外,還存在以下不同之處:
- 沒有顯式地創(chuàng)建對(duì)象;
- 直接將屬性和方法賦給了this對(duì)象;
- 沒有return語句
此外,還應(yīng)該注意到函數(shù)名Person使用的是大寫字母P。按照慣例,構(gòu)造函數(shù)始終都應(yīng)該以一個(gè)大寫字母開頭,而非構(gòu)造函數(shù)則應(yīng)該以一個(gè)小寫字母開頭。這個(gè)做法借鑒自其他OO語言,主要是為了區(qū)別于ES中其他函數(shù);因?yàn)闃?gòu)造函數(shù)本身也是函數(shù),只不過可以用來創(chuàng)建對(duì)象而已。
要?jiǎng)?chuàng)建Person的新實(shí)例,必須使用new操作符。以這種方式調(diào)用你構(gòu)造函數(shù)實(shí)際上會(huì)經(jīng)歷一下4個(gè)步驟:
(1)創(chuàng)建一個(gè)新對(duì)象
(2)將構(gòu)造函數(shù)的作用域賦給新對(duì)象(因此this就指向了這個(gè)新對(duì)象)
(3)執(zhí)行構(gòu)造函數(shù)中的代碼(為這個(gè)新對(duì)象添加屬性)
(4)返回新對(duì)象。
在前面的例子的最后,person1和person2分別保存著Person的一個(gè)不同的實(shí)例。這兩個(gè)對(duì)象都有一個(gè)constructor(構(gòu)造函數(shù))屬性,該屬性指向Person。
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
創(chuàng)建自定義的構(gòu)造函數(shù)意味著將來可以將它的實(shí)例標(biāo)識(shí)為一種特定的類型;而這正是構(gòu)造函數(shù)模式勝過工廠模式的地方。
1、將構(gòu)造函數(shù)當(dāng)做函數(shù)
構(gòu)造函數(shù)與其他函數(shù)的唯一區(qū)別,就在于調(diào)用它們的方式不同。不過,構(gòu)造函數(shù)畢竟也是函數(shù)。不存在定義構(gòu)造函數(shù)的特殊語法。任何函數(shù),只要通過new操作符來調(diào)用,那它就可以作為構(gòu)造函數(shù);而任何函數(shù),如果不通過new操作符來調(diào)用,那它跟普通函數(shù)也不會(huì)有什么兩樣:
// 當(dāng)作構(gòu)造函數(shù)使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"
// 作為普通函數(shù)調(diào)用
Person("Greg", 27, "Doctor"); // 添加到window
window.sayName(); //"Greg"
// 在另一個(gè)對(duì)象的作用域中調(diào)用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"
2、構(gòu)造函數(shù)的問題
構(gòu)造函數(shù)模式雖然好用,但也并非沒有缺點(diǎn)。使用構(gòu)造函數(shù)的主要問題,就是每個(gè)方法都要在每個(gè)實(shí)例上重新創(chuàng)建一遍。在前面的例子中,person1和person2都有一個(gè)名為sayName()的方法,但那兩個(gè)方法不是同一個(gè)Function的實(shí)例。不要忘了——ECMAScript中的函數(shù)是對(duì)象,因此每定義一個(gè)函數(shù),也就是實(shí)例化了一個(gè)對(duì)象。
然而,創(chuàng)建兩個(gè)完全同樣任務(wù)的Function實(shí)例的確沒有必要;況且有this對(duì)象在,根本不用在執(zhí)行代碼前就把函數(shù)綁定到特定對(duì)象上面。因此,大可像下面這樣,通過把函數(shù)定義轉(zhuǎn)移到構(gòu)造函數(shù)外部來解決這個(gè)問題:
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("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
在這個(gè)例子中,我們把sayName()函數(shù)的定義轉(zhuǎn)移到了構(gòu)造函數(shù)外部。而在構(gòu)造函數(shù)內(nèi)部,我們將sayName屬性設(shè)置成等于全局的sayName函數(shù)。這樣一來,由于sayName包含的是一個(gè)指向函數(shù)的指針,因此person1和person2對(duì)象就共享了在全局作用域中定義的同一個(gè)sayName()函數(shù)。這樣做確實(shí)解決了兩個(gè)函數(shù)做同一件事的問題,可是新問題又來了:在全局作用域中定義的函數(shù)實(shí)際上只能被某個(gè)對(duì)象調(diào)用,這讓全局作用域有點(diǎn)名不副實(shí)。而更讓人無法接受的是:如果對(duì)象需要定義很多方法,那么就要定義很多個(gè)全局函數(shù),于是我們這個(gè)自定義的引用類型就絲毫沒有封裝性可言了。好在,這些問題可以通過使用原型模式來解決。
原型模式
我們創(chuàng)建的每個(gè)函數(shù)都有一個(gè)prototype(原型)屬性,這個(gè)屬性是一個(gè)指針,指向一個(gè)對(duì)象,而這個(gè)對(duì)象的用途是包含可以由特定類型的所有實(shí)例共享的屬性和方法。如果按照字面意思來理解,那么prototype就是通過調(diào)用構(gòu)造函數(shù)而創(chuàng)建的那個(gè)對(duì)象實(shí)例的原型對(duì)象。使用原型對(duì)象的好處是可以讓所有對(duì)象實(shí)例共享它所包含的屬性和方法。換句話說,不必在構(gòu)造函數(shù)中定義對(duì)象實(shí)例的信息,而是可以將這些信息直接添加到原型對(duì)象中:
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
在此,我們將sayName()方法和所有屬性直接添加到了Person的prototype屬性中,構(gòu)造函數(shù)變成了空函數(shù)。即使如此,也仍然可以通過調(diào)用構(gòu)造函數(shù)來創(chuàng)建新對(duì)象,而且新對(duì)象還會(huì)具有相同的屬性和方法。但與構(gòu)造函數(shù)模式不同的是,新對(duì)象的這些屬性和方法是由所有實(shí)例共享的。換句話說,person1和person2訪問的都是同一組屬性和同一個(gè)sayName()函數(shù)。要理解原型模式的工作原理,必須先理解ES中原型對(duì)象的性質(zhì)。
1、理解原型對(duì)象
無論什么時(shí)候,只要?jiǎng)?chuàng)建了一個(gè)新函數(shù),就會(huì)根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個(gè)prototype屬性,這個(gè)屬性指向函數(shù)的原型對(duì)象。在默認(rèn)情況下,所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè)constructor(構(gòu)造函數(shù)),這個(gè)屬性包含一個(gè)指向prototype屬性所在函數(shù)的指針。就拿前面的例子來說,Person.prototype.constructor指向Person。而通過這個(gè)構(gòu)造函數(shù),我們還可繼續(xù)為原型對(duì)象添加其他屬性和方法。
創(chuàng)建了自定義的構(gòu)造函數(shù)之后,其原型對(duì)象默認(rèn)只會(huì)取得constructor屬性;至于其他方法,則都是從Object繼承而來的。當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例后,該實(shí)例的內(nèi)部將包含一個(gè)指針(內(nèi)部屬性),指向構(gòu)造函數(shù)的原型對(duì)象。ECMA-262第5版中管這個(gè)指針叫[[Prototype]]。雖然在腳本中沒有標(biāo)準(zhǔn)的方式訪問[[Prototype]],但Firefox、Safari和Chrome在每個(gè)對(duì)象上都支持一個(gè)屬性proto;而在其他實(shí)現(xiàn)中,這個(gè)屬性對(duì)腳本則是完全不可見的。不過,要明確的真正重要的一點(diǎn)就是,這個(gè)連接存在于實(shí)例與構(gòu)造函數(shù)的原型對(duì)象之間,而不是存在于實(shí)例與構(gòu)造函數(shù)之間。

上圖展示了Person構(gòu)造函數(shù)、Person的原型屬性以及Person現(xiàn)有的兩個(gè)實(shí)例之間的關(guān)系。在此,Person.prototype指向了原型對(duì)象,而Person.prototype.constructor又指回了Person。原型對(duì)象中除了包含constructor屬性之外,還包含后來添加的其他屬性。Person的每個(gè)實(shí)例——person1和person2都包含一個(gè)內(nèi)部屬性,該屬性僅僅指向了Person.prototype;換句話說,它們與構(gòu)造函數(shù)沒有直接的關(guān)系。此外,要格外注意的是,雖然這兩個(gè)實(shí)例都不包含屬性和方法,但我們卻可以調(diào)用person.sayName()。這是通過查找對(duì)象屬性的過程來實(shí)現(xiàn)的。
雖然在所有實(shí)現(xiàn)中都無法訪問到[[Prototype]],但可以通過isPrototypeOf()方法來確定對(duì)象之間是否存在這種關(guān)系。從本質(zhì)上講,如果[[Prototype]]指向調(diào)用isPrototypeOf()方法的對(duì)象(Person.prototype),那么這個(gè)方法就返回true:
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
ES5增加了一個(gè)新方法,叫Object.getPrototypeOf(),在所有支持的實(shí)現(xiàn)中,這個(gè)方法返回[[Prototype]]的值:
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"
使用Object.getPrototypeOf()可以方便地取得一個(gè)對(duì)象的原型,而這在利用原型實(shí)現(xiàn)繼承(后面會(huì)討論)的情況下是非常重要的。支持這個(gè)方法的瀏覽器有IE9+、Firefox3.5+、Safari 5+、Opera12+和Chrome。
每當(dāng)代碼讀取某個(gè)對(duì)象的某個(gè)屬性時(shí),都會(huì)執(zhí)行一次搜索,目標(biāo)是具有給定名字的屬性。搜索首先從對(duì)象實(shí)例本身開始。如果在實(shí)例中找到了具有給定名字的屬性,則返回該屬性的值;如果沒有找到,則繼續(xù)搜索指針指向的原型對(duì)象,在原型對(duì)象中查找具有給定名字的屬性。如果在原型對(duì)象中找到了這個(gè)屬性,則返回該屬性的值。這正是多個(gè)對(duì)象實(shí)例共享原型所保存的屬性和方法的基本原理。
前面提到過,原型最初只包含constructor屬性,而該屬性也是共享的,因此可以通過對(duì)象實(shí)例訪問。
雖然可以通過對(duì)象實(shí)例訪問保存在原型中的值,但卻不能通過對(duì)象實(shí)例重寫原型中的值。如果我們?cè)趯?shí)例中添加了一個(gè)屬性,而該屬性與實(shí)例原型中的一個(gè)屬性同名,那么就在實(shí)例中創(chuàng)建該屬性,該屬性將會(huì)屏蔽原型中的那個(gè)屬性:
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name); //"Greg"來自實(shí)例
alert(person2.name); //"Nicholas"來自原型
當(dāng)為對(duì)象實(shí)例添加一個(gè)屬性時(shí),這個(gè)屬性就會(huì)屏蔽原型對(duì)象中保存的同名屬性;換句話說,添加這個(gè)屬性只會(huì)阻止我們?cè)L問原型中的那個(gè)屬性,但不會(huì)修改那個(gè)屬性。即使將這個(gè)屬性設(shè)置為null,也只會(huì)在實(shí)例中設(shè)置這個(gè)屬性,而不會(huì)回復(fù)其指向原型的連接。不過,使用delete操作符則可以完全刪除實(shí)例屬性,從而讓我們能夠重新訪問原型中的屬性:
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name); //"Greg"來自實(shí)例
alert(person2.name); //Nicholas"來自原型
delete person1.name;
alert(person1.name); //"Nicholas"來自原型
使用hasOwnProperty()方法可以檢測一個(gè)屬性是存在于實(shí)例中,還是存在于原型中。這個(gè)方法(不要忘了它是從Object繼承來的)只在給定屬性存在于對(duì)象實(shí)例中時(shí),才會(huì)返回true:
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
person1.name = "Greg";
alert(person1.name); //“Greg”來自實(shí)例
alert(person1.hasOwnProperty("name")); //true
alert(person2.name); //“Nicholas”來自原型
alert(person2.hasOwnProperty("name")); //false
delete person1.name;
alert(person1.name); //“Nicholas”來自原型
alert(person1.hasOwnProperty("name")); //false
ES5的Object.getOwnPropertyDescriptor()方法只能用于實(shí)例屬性,要取得原型屬性的描述符,必須直接在原型對(duì)象上調(diào)用Object.getOwnPropertyDescriptor()。