創(chuàng)建對象
雖然 Object 構(gòu)造函數(shù)或?qū)ο笞置媪慷伎梢杂脕韯?chuàng)建單個對象躁锁,但這些方式有個明顯的缺點:使用同 一個接口創(chuàng)建很多對象,會產(chǎn)生大量的重復(fù)代碼卵史。為解決這個問題战转,人們開始使用工廠模式的一種變體。
工廠模式
工廠模式是軟件工程領(lǐng)域一種廣為人知的設(shè)計模式程腹,這種模式抽象了創(chuàng)建具體對象的過程匣吊。考慮到在 ECMAScript中無法創(chuàng)建類寸潦,開發(fā)人員 就發(fā)明了一種函數(shù)色鸳,用函數(shù)來封裝以特定接口創(chuàng)建對象的細(xì)節(jié),如下面的例子所示见转。
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");
函數(shù) createPerson()能夠根據(jù)接受的參數(shù)來構(gòu)建一個包含所有必要信息的 Person 對象命雀。可以無 數(shù)次地調(diào)用這個函數(shù)斩箫,而每次它都會返回一個包含三個屬性一個方法的對象吏砂。工廠模式雖然解決了創(chuàng)建 多個相似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)乘客。隨著 JavaScript 的發(fā)展狐血,又一個新模式出現(xiàn)了。
構(gòu)造函數(shù)模式
unction 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");
在這個例子中易核,Person()函數(shù)取代了 createPerson()函數(shù)匈织。我們注意到,Person()中的代碼 除了與 createPerson()中相同的部分外,還存在以下不同之處:
- 沒有顯式地創(chuàng)建對象
- 直接將屬性和方法賦給了 this 對象
- 沒有 return 語句
此外缀匕,還應(yīng)該注意到函數(shù)名 Person 使用的是大寫字母 P纳决。按照慣例,構(gòu)造函數(shù)始終都應(yīng)該以一個 大寫字母開頭乡小,而非構(gòu)造函數(shù)則應(yīng)該以一個小寫字母開頭。這個做法借鑒自其他 OO語言胜榔,主要是為了 區(qū)別于 ECMAScript中的其他函數(shù)零远;因為構(gòu)造函數(shù)本身也是函數(shù)摔癣,只不過可以用來創(chuàng)建對象而已择浊。
要創(chuàng)建 Person 的新實例,必須使用 new 操作符担孔。以這種方式調(diào)用構(gòu)造函數(shù)實際上會經(jīng)歷以下 4 個步驟:
- 創(chuàng)建一個新對象
- 2.將構(gòu)造函數(shù)的作用域賦給新對象(因此 this 就指向了這個新對象)
- 3.執(zhí)行構(gòu)造函數(shù)中的代碼(為這個新對象添加屬性)
- 4.返回新對象
在前面例子的后吃警,person1 和 person2 分別保存著 Person 的一個不同的實例拌消。這兩個對象都 有一個 constructor(構(gòu)造函數(shù))屬性墩崩,該屬性指向 Person鹦筹,如下所示:
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
創(chuàng)建自定義的構(gòu)造函數(shù)意味著將來可以將它的實例標(biāo)識為一種特定的類型盛龄;而這正是構(gòu)造函數(shù)模式 勝過工廠模式的地方。在這個例子中匿值,person1 和 person2 之所以同時是 Object 的實例挟憔,是因為所 有對象均繼承自 Object(詳細(xì)內(nèi)容稍后討論)。
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ù)也不會有什么兩樣脊僚。例如辽幌,前面例子中定義 的 Person()函數(shù)可以通過下列任何一種方式來調(diào)用:
// 當(dāng)作構(gòu)造函數(shù)使用
let person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"
// 作為普通函數(shù)調(diào)用
Person("Greg", 27, "Doctor"); // 添加到 window
window.sayName(); //"Greg"
// 在另一個對象的作用域中調(diào)用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"
這個例子中的前兩行代碼展示了構(gòu)造函數(shù)的典型用法,即使用 new 操作符來創(chuàng)建一個新對象成玫。接下 來的兩行代碼展示了不使用new操作符調(diào)用Person()會出現(xiàn)什么結(jié)果:屬性和方法都被添加給window 對象了虽画。有讀者可能還記得码撰,當(dāng)在全局作用域中調(diào)用一個函數(shù)時脖岛,this 對象總是指向 Global 對象(在 瀏覽器中就是 window 對象)。因此绍在,在調(diào)用完函數(shù)之后,可以通過 window 對象來調(diào)用 sayName()方 法霸奕,并且還返回了"Greg"适揉。后嫉嘀,也可以使用 call()(或者 apply())在某個特殊對象的作用域中 調(diào)用Person()函數(shù)汤善。這里是在對象o的作用域中調(diào)用的不狮,因此調(diào)用后o就擁有了所有屬性和sayName() 方法推掸。
這里補充一下call():
Function.call(obj,[param1[,param2[,…[,paramN]]]])
obj:這個對象將代替Function類里this對象
params:這個是一個參數(shù)列表,它將作為參數(shù)傳給Function
2.構(gòu)造函數(shù)的問題
構(gòu)造函數(shù)模式雖然好用谅畅,但也并非沒有缺點。使用構(gòu)造函數(shù)的主要問題粘优,就是每個方法都要在每個 實例上重新創(chuàng)建一遍丹墨。在前面的例子中,person1 和 person2 都有一個名為 sayName()的方法王财,但那 兩個方法不是同一個 Function 的實例搪搏。不要忘了——ECMAScript中的函數(shù)是對象论颅,因此每定義一個 函數(shù)恃疯,也就是實例化了一個對象。從邏輯角度講盾鳞,此時的構(gòu)造函數(shù)也可以這樣定義。
即
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)"); // 與聲明函數(shù)在邏輯上是等價的
}
從這個角度上來看構(gòu)造函數(shù)瞻离,更容易明白每個 Person 實例都包含一個不同的 Function 實例(以 顯示 name 屬性)的本質(zhì)腾仅。說明白些,以這種方式創(chuàng)建函數(shù)套利,會導(dǎo)致不同的作用域鏈和標(biāo)識符解析推励,但 創(chuàng)建 Function 新實例的機制仍然是相同的。因此肉迫,不同實例上的同名函數(shù)是不相等的,以下代碼可以 證明這一點
alert(person1.sayName == person2.sayName); //false
然而,創(chuàng)建兩個完成同樣任務(wù)的 Function 實例的確沒有必要联四;況且有 this 對象在亿卤,根本不用在 執(zhí)行代碼前就把函數(shù)綁定到特定對象上面屹堰。因此,大可像下面這樣,通過把函數(shù)定義轉(zhuǎn)移到構(gòu)造函數(shù)外 部來解決這個問題斥杜。
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");
在這個例子中乖阵,我們把 sayName()函數(shù)的定義轉(zhuǎn)移到了構(gòu)造函數(shù)外部对蒲。而在構(gòu)造函數(shù)內(nèi)部,我們 將 sayName 屬性設(shè)置成等于全局的 sayName 函數(shù)刚操。這樣一來华蜒,由于 sayName 包含的是一個指向函數(shù) 的指針,因此 person1 和 person2 對象就共享了在全局作用域中定義的同一個 sayName()函數(shù)溉知。這 樣做確實解決了兩個函數(shù)做同一件事的問題,可是新問題又來了:在全局作用域中定義的函數(shù)實際上只 能被某個對象調(diào)用恒傻,這讓全局作用域有點名不副實。而更讓人無法接受的是:如果對象需要定義很多方 法捐晶,那么就要定義很多個全局函數(shù),于是我們這個自定義的引用類型就絲毫沒有封裝性可言了池凄。好在柏副, 這些問題可以通過使用原型模式來解決(說白了都是為了性能荔泳,減少不必要的內(nèi)存空間使用)擎椰。
原型模式
我們創(chuàng)建的每個函數(shù)都有一個 prototype(原型)屬性趾代,這個屬性是一個指針摆马,指向一個對象棉磨, 而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。如果按照字面意思來理解学辱,那 么 prototype 就是通過調(diào)用構(gòu)造函數(shù)而創(chuàng)建的那個對象實例的原型對象乘瓤。使用原型對象的好處是可以 讓所有對象實例共享它所包含的屬性和方法。換句話說策泣,不必在構(gòu)造函數(shù)中定義對象實例的信息衙傀,而是 可以將這些信息直接添加到原型對象中,如下面的例子所示:
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){
alert(this.name);
};
let person1 = new Person();
person1.sayName(); //"Nicholas"
let person2 = new Person();
person2.sayName(); //"Nicholas"
console.log(person1.sayName == person2.sayName); //true
在此萨咕,我們將 sayName()方法和所有屬性直接添加到了 Person 的 prototype 屬性中差油,構(gòu)造函數(shù) 變成了空函數(shù)。即使如此,也仍然可以通過調(diào)用構(gòu)造函數(shù)來創(chuàng)建新對象蓄喇,而且新對象還會具有相同的屬 性和方法发侵。但與構(gòu)造函數(shù)模式不同的是,新對象的這些屬性和方法是由所有實例共享的妆偏。換句話說刃鳄, person1 和 person2 訪問的都是同一組屬性和同一個 sayName()函數(shù)。要理解原型模式的工作原理钱骂, 必須先理解 ECMAScript中原型對象的性質(zhì)叔锐。
- 理解原型對象
無論什么時候,只要創(chuàng)建了一個新函數(shù)见秽,就會根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個 prototype 屬性愉烙,這個屬性指向函數(shù)的原型對象。在默認(rèn)情況下解取,所有原型對象都會自動獲得一個 constructor (構(gòu)造函數(shù))屬性步责,這個屬性包含一個指向 prototype 屬性所在函數(shù)的指針。就拿前面的例子來說禀苦, Person.prototype. constructor 指向 Person蔓肯。而通過這個構(gòu)造函數(shù),我們還可繼續(xù)為原型對象 添加其他屬性和方法振乏。
創(chuàng)建了自定義的構(gòu)造函數(shù)之后蔗包,其原型對象默認(rèn)只會取得 constructor 屬性;至于其他方法慧邮,則 都是從 Object 繼承而來的调限。當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個新實例后,該實例的內(nèi)部將包含一個指針(內(nèi)部 屬性)误澳,指向構(gòu)造函數(shù)的原型對象旧噪。ECMA-262第 5版中管這個指針叫[[Prototype]]。雖然在腳本中 沒有標(biāo)準(zhǔn)的方式訪問[[Prototype]]脓匿,但 Firefox、Safari 和 Chrome 在每個對象上都支持一個屬性 proto宦赠;而在其他實現(xiàn)中陪毡,這個屬性對腳本則是完全不可見的。不過勾扭,要明確的真正重要的一點就 是毡琉,這個連接存在于實例與構(gòu)造函數(shù)的原型對象之間,而不是存在于實例與構(gòu)造函數(shù)以前面使用 Person 構(gòu)造函數(shù)和 Person.prototype 創(chuàng)建實例的代碼為例妙色,下圖展示了各個對 象之間的關(guān)系桅滋。
之間。
圖 6-1展示了 Person 構(gòu)造函數(shù)、Person 的原型屬性以及 Person 現(xiàn)有的兩個實例之間的關(guān)系丐谋。 在此芍碧,Person.prototype指向了原型對象,而Person.prototype.constructor又指回了Person号俐。 原型對象中除了包含 constructor 屬性之外泌豆,還包括后來添加的其他屬性。Person 的每個實例—— person1 和 person2 都包含一個內(nèi)部屬性吏饿,該屬性僅僅指向了 Person.prototype踪危;換句話說,它們 與構(gòu)造函數(shù)沒有直接的關(guān)系猪落。此外贞远,要格外注意的是,雖然這兩個實例都不包含屬性和方法笨忌,但我們卻可以調(diào)用 person1.sayName()蓝仲。這是通過查找對象屬性的過程來實現(xiàn)的。
雖然在所有實現(xiàn)中都無法訪問到[[Prototype]]蜜唾,但可以通過 isPrototypeOf()方法來確定對象之 間是否存在這種關(guān)系杂曲。從本質(zhì)上講,如果[[Prototype]]指向調(diào)用 isPrototypeOf()方法的對象 (Person.prototype)袁余,那么這個方法就返回 true擎勘,如下所示:
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
這里,我們用原型對象的 isPrototypeOf()方法測試了 person1 和 person2颖榜。因為它們內(nèi)部都 有一個指向 Person.prototype 的指針棚饵,因此都返回了 true
ECMAScript 5增加了一個新方法,叫 Object.getPrototypeOf()掩完,在所有支持的實現(xiàn)中噪漾,這個 方法返回[[Prototype]]的值。例如:
alert(Object.getPrototypeOf(person1) === Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"
這里的第一行代碼只是確定 Object.getPrototypeOf()返回的對象實際就是這個對象的原型且蓬。
第二行代碼取得了原型對象中 name 屬性的值欣硼,也就是"Nicholas"。使用 Object.getPrototypeOf() 可以方便地取得一個對象的原型恶阴,而這在利用原型實現(xiàn)繼承(本系列稍后會討論)的情況下是非常重要的诈胜。 支持這個方法的瀏覽器有 IE9+、Firefox 3.5+冯事、Safari 5+焦匈、Opera 12+和 Chrome。
重點來了:
每當(dāng)代碼讀取某個對象的某個屬性時昵仅,都會執(zhí)行一次搜索缓熟,目標(biāo)是具有給定名字的屬性。搜索首先 從對象實例本身
開始。如果在實例中找到了具有給定名字的屬性够滑,則返回該屬性的值垦写;如果沒有找到,則繼續(xù)搜索指針指向的原型對象
版述,在原型對象中查找具有給定名字的屬性梯澜。如果在原型對象中找到了這 個屬性,則返回該屬性的值渴析。也就是說晚伙,在我們調(diào)用 person1.sayName()的時候,會先后執(zhí)行兩次搜索
俭茧。首先咆疗,解析器會問:“實例 person1 有 sayName 屬性嗎?”答:“沒有母债∥绱牛”然后,它繼續(xù)搜索毡们,再 問:“person1 的原型有 sayName 屬性嗎迅皇?”答:“有⊙萌郏”于是登颓,它就讀取那個保存在原型對象中的函 數(shù)。當(dāng)我們調(diào)用 person2.sayName()時红氯,將會重現(xiàn)相同的搜索過程框咙,得到相同的結(jié)果。而這正是多個 對象實例共享原型所保存的屬性和方法的基本原理痢甘。
雖然可以通過對象實例訪問保存在原型中的值喇嘱,但卻不能通過對象實例重寫原型中的值。如果我們 在實例中添加了一個屬性塞栅,而該屬性與實例原型中的一個屬性同名者铜,那我們就在實例中創(chuàng)建該屬性,該 屬性將會屏蔽原型中的那個屬性放椰。來看下面的例子作烟。
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"——來自實例
alert(person2.name); //"Nicholas"——來自原型
在這個例子中,person1 的 name 被一個新值給屏蔽了庄敛。但無論訪問 person1.name 還是訪問 person2.name 都能夠正常地返回值,即分別是"Greg"(來自對象實例)和"Nicholas"(來自原型)科汗。 當(dāng)在 alert()中訪問 person1.name 時藻烤,需要讀取它的值,因此就會在這個實例上搜索一個名為 name 的屬性。這個屬性確實存在怖亭,于是就返回它的值而不必再搜索原型了
涎显。當(dāng)以同樣的方式訪問 person2. name 時,并沒有在實例上發(fā)現(xiàn)該屬性兴猩,因此就會繼續(xù)搜索原型
期吓,結(jié)果在那里找到了 name 屬性。
重點又來了
當(dāng)為對象實例添加一個屬性時倾芝,這個屬性就會屏蔽原型對象中保存的同名屬性讨勤;換句話說,添加這 個屬性只會阻止我們訪問原型中的那個屬性晨另,但不會修改那個屬性潭千。即使將這個屬性設(shè)置為 null,也只會在實例中設(shè)置這個屬性借尿,而不會恢復(fù)其指向原型的連接刨晴。
不過,使用 delete 操作符則可以完全刪 除實例屬性路翻,從而讓我們能夠重新訪問原型中的屬性狈癞,如下所示。
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"——來自實例
alert(person2.name); //"Nicholas"——來自原型
delete person1.name;
alert(person1.name); //"Nicholas"——來自原型
在這個修改后的例子中茂契,我們使用 delete 操作符刪除了 person1.name蝶桶,之前它保存的"Greg" 值屏蔽了同名的原型屬性。把它刪除以后账嚎,就恢復(fù)了對原型中 name 屬性的連接莫瞬。因此,接下來再調(diào)用 person1.name 時郭蕉,返回的就是原型中 name 屬性的值了疼邀。
使用 hasOwnProperty()
方法可以檢測一個屬性是存在于實例中,還是存在于原型中召锈。這個方法(不 要忘了它是從 Object 繼承來的)只在給定屬性存在于對象實例中時旁振,才會返回 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"——來自實例
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
通過使用 hasOwnProperty()方法涨岁,什么時候訪問的是實例屬性拐袜,什么時候訪問的是原型屬性就 一清二楚了。調(diào)用 person1.hasOwnProperty( "name")時梢薪,只有當(dāng) person1 重寫 name 屬性后才會 返回 true蹬铺,因為只有這時候 name 才是一個實例屬性,而非原型屬性秉撇。圖 6-2展示了上面例子在不同情 況下的實現(xiàn)與原型的關(guān)系(為了簡單起見甜攀,圖中省略了與 Person 構(gòu)造函數(shù)的關(guān)系)秋泄。
ECMAScript 5 的 Object.getOwnPropertyDescriptor()方法只能用于實例屬 性,要取得原型屬性的描述符规阀,必須直接在原型對象上調(diào)用 Object.getOwnPropertyDescriptor()方法恒序。
2.原型與 in 操作符
重點重點
有兩種方式使用 in 操作符:單獨使用和在 for-in 循環(huán)中使用。在單獨使用時谁撼,in 操作符會在通 過對象能夠訪問給定屬性時返回 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
alert("name" in person1); //true
person1.name = "Greg";
alert(person1.name); //"Greg" ——來自實例
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1); //true
alert(person2.name); //"Nicholas" ——來自原型
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2); //true
delete person1.name;
alert(person1.name); //"Nicholas" ——來自原型
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
在以上代碼執(zhí)行的整個過程中,name 屬性要么是直接在對象上訪問到的墨榄,要么是通過原型訪問到 的玄糟。因此,調(diào)用"name" in person1 始終都返回 true袄秩,無論該屬性存在于實例中還是存在于原型中阵翎。 同時使用 hasOwnProperty()方法和 in 操作符,就可以確定該屬性到底是存在于對象中之剧,還是存在于 原型中郭卫,如下所示。
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
由于 in 操作符只要通過對象能夠訪問到屬性就返回 true背稼,hasOwnProperty()只在屬性存在于 實例中時才返回 true贰军,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以確 定屬性是原型中的屬性蟹肘。下面來看一看上面定義的函數(shù) hasPrototypeProperty()的用法词疼。
在使用 for-in 循環(huán)時,返回的是所有能夠通過對象訪問的帘腹、可枚舉的(enumerated)屬性贰盗,其中 既包括存在于實例中的屬性,也包括存在于原型中的屬性阳欲。屏蔽了原型中不可枚舉屬性(即將 [[Enumerable]]標(biāo)記為 false 的屬性)的實例屬性也會在 for-in 循環(huán)中返回舵盈,因為根據(jù)規(guī)定,所 有開發(fā)人員定義的屬性都是可枚舉的球化。
要取得對象上所有可枚舉的實例屬性秽晚,可以使用 ECMAScript 5的 Object.keys()方法。這個方法 接收一個對象作為參數(shù)筒愚,返回一個包含所有可枚舉屬性的字符串?dāng)?shù)組刊橘。例如:
function Person(){ }
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var keys = Object.keys(Person.prototype);
alert(keys); //"name,age,job,sayName"
var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
alert(p1keys); //"name,age"
這里溯饵,變量 keys 中將保存一個數(shù)組礼仗,數(shù)組中是字符串"name"、"age"稳懒、"job"和"sayName"。這 個順序也是它們在 for-in 循環(huán)中出現(xiàn)的順序。如果是通過 Person 的實例調(diào)用滋戳,則 Object.keys() 返回的數(shù)組只包含"name"和"age"這兩個實例屬性。
如果你想要得到所有實例屬性倔约,無論它是否可枚舉秃殉,都可以使用 Object.getOwnPropertyNames() 方法。
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); //"constructor,name,age,job,sayName"
3.更簡單的寫法
前面例子中每添加一個屬性和方法就要敲一遍 Person.prototype浸剩。為減少
不必要的輸入钾军,也為了從視覺上更好地封裝原型的功能,更常見的做法是用一個包含所有屬性和方法的
對象字面量來重寫整個原型對象绢要,如下面的例子所示:
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
在上面的代碼中吏恭,我們將 Person.prototype 設(shè)置為等于一個以對象字面量形式創(chuàng)建的新對象。
最終結(jié)果相同重罪,但有一個例外:constructor
屬性不再指向 Person 了樱哼。
每創(chuàng)建一個函數(shù),就會同時創(chuàng)建它的 prototype 對象剿配,這個對象也會自動獲得constructor
屬性搅幅。而我們在
這里使用的語法,本質(zhì)上完全重寫了默認(rèn)的prototype
對象呼胚,因此constructor
屬性也就變成了新
對象的constructor
屬性(指向Object
構(gòu)造函數(shù))茄唐,不再指向 Person 函數(shù)。此時蝇更,盡管instanceof
操作符還能返回正確的結(jié)果沪编,但通過constructor
已經(jīng)無法確定對象的類型了,如下所示年扩。
var friend = new Person();
alert(friend instanceof Object); //true
alert(friend instanceof Person); //true
alert(friend.constructor == Person); //false
alert(friend.constructor == Object); //true
在此蚁廓,用 instanceof 操作符測試 Object 和 Person 仍然返回 true,但 constructor 屬性則
等于 Object 而不等于 Person 了常遂。如果 constructor 的值真的很重要纳令,可以像下面這樣特意將它設(shè)
置回適當(dāng)?shù)闹怠?/p>
function Person(){
}
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
以上代碼特意包含了一個 constructor 屬性,并將它的值設(shè)置為 Person克胳,從而確保了通過該屬
性能夠訪問到適當(dāng)?shù)闹怠?/p>
注意平绩,以這種方式重設(shè)
constructor
屬性會導(dǎo)致它的[[Enumerable]]
特性被設(shè)置為 true。默認(rèn)
情況下漠另,原生的constructor
屬性是不可枚舉的捏雌,因此如果你使用兼容 ECMAScript 5 的 JavaScript 引
擎,可以試一試 Object.defineProperty()笆搓。
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
//重設(shè)構(gòu)造函數(shù)性湿,只適用于 ECMAScript 5 兼容的瀏覽器
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
4.原型的動態(tài)性
由于在原型中查找值的過程是一次搜索纬傲,因此我們對原型對象所做的任何修改都能夠立即從實例上
反映出來——即使是先創(chuàng)建了實例后修改原型也照樣如此。請看下面的例子肤频。
var person= new Person();
Person.prototype.sayHi = function(){
alert("hi");
};
person.sayHi(); //"hi"(沒有問題L纠ā)
下面是重點:
以上代碼先創(chuàng)建了 Person 的一個實例,并將其保存在 person 中宵荒。然后汁雷,下一條語句在 Person.
prototype 中添加了一個方法 sayHi()。即使 person 實例是在添加新方法之前創(chuàng)建的报咳,但它仍然可
以訪問這個新方法侠讯。其原因可以歸結(jié)為實例與原型之間的松散連接關(guān)系。當(dāng)我們調(diào)用 person.sayHi()
時暑刃,首先會在實例中搜索名為 sayHi 的屬性厢漩,在沒找到的情況下,會繼續(xù)搜索原型岩臣。因為實例與原型
之間的連接只不過是一個指針溜嗜,而非一個副本,因此就可以在原型中找到新的 sayHi 屬性并返回保存
在那里的函數(shù)架谎。盡管可以隨時為原型添加屬性和方法粱胜,并且修改能夠立即在所有對象實例中反映出來,但如果是重
寫整個原型對象狐树,那么情況就不一樣了焙压。我們知道,調(diào)用構(gòu)造函數(shù)時會為實例添加一個指向最初原型的
[[Prototype]]指針抑钟,而把原型修改為另外一個對象就等于切斷了構(gòu)造函數(shù)與最初原型之間的聯(lián)系涯曲。
請記住:實例中的指針僅指向原型在塔,而不指向構(gòu)造函數(shù)幻件。看下面的例子蛔溃。
function Person(){
}
var friend = new Person();
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
friend.sayName(); //error
在這個例子中绰沥,我們先創(chuàng)建了 Person 的一個實例,然后又重寫了其原型對象贺待。然后在調(diào)用
friend.sayName()時發(fā)生了錯誤徽曲,因為 friend 指向的原型中不包含以該名字命名的屬性。圖 6-3 展示了這個過程的內(nèi)幕麸塞。
從圖 6-3 可以看出秃臣,重寫原型對象切斷了現(xiàn)有原型與任何之前已經(jīng)存在的對象實例之間的聯(lián)系;它
們引用的仍然是最初的原型。
- 原生對象的原型
原型模式的重要性不僅體現(xiàn)在創(chuàng)建自定義類型方面奥此,就連所有原生的引用類型弧哎,都是采用這種模式
創(chuàng)建的。所有原生引用類型(Object稚虎、Array撤嫩、String,等等)都在其構(gòu)造函數(shù)的原型上定義了方法蠢终。
例如非洲,在 Array.prototype 中可以找到 sort()方法,而在 String.prototype 中可以找到
substring()方法蜕径,如下所示。
alert(typeof Array.prototype.sort); //"function"
alert(typeof String.prototype.substring); //"function"
通過原生對象的原型败京,不僅可以取得所有默認(rèn)方法的引用兜喻,而且也可以定義新方法∩穆螅可以像修改自
定義對象的原型一樣修改原生對象的原型朴皆,因此可以隨時添加方法。下面的代碼就給基本包裝類型
String 添加了一個名為 startsWith()的方法泛粹。
String.prototype.startsWith = function (text) {
return this.indexOf(text) == 0;
};
var msg = "Hello world!";
alert(msg.startsWith("Hello")); //true
這里新定義的 startsWith()方法會在傳入的文本位于一個字符串開始時返回 true遂铡。既然方法被
添加給了 String.prototype,那么當(dāng)前環(huán)境中的所有字符串就都可以調(diào)用它晶姊。由于 msg 是字符串扒接,
而且后臺會調(diào)用 String 基本包裝函數(shù)創(chuàng)建這個字符串,因此通過 msg 就可以調(diào)用 startsWith()方法.
- 原型對象的問題
原型模式也不是沒有缺點们衙。首先钾怔,它省略了為構(gòu)造函數(shù)傳遞初始化參數(shù)這一環(huán)節(jié),結(jié)果所有實例在
默認(rèn)情況下都將取得相同的屬性值蒙挑。雖然這會在某種程度上帶來一些不方便宗侦,但還不是原型的最大問題。
原型模式的最大問題是由其共享的本性所導(dǎo)致的忆蚀。
原型中所有屬性是被很多實例共享的矾利,這種共享對于函數(shù)非常合適。對于那些包含基本值的屬性倒
也說得過去馋袜,畢竟(如前面的例子所示)男旗,通過在實例上添加一個同名屬性,可以隱藏原型中的對應(yīng)屬
性欣鳖。然而剑肯,對于包含引用類型值的屬性來說,問題就比較突出了观堂。來看下面的例子让网。
function Person(){
}
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
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
在此呀忧,Person.prototype 對象有一個名為 friends 的屬性,該屬性包含一個字符串?dāng)?shù)組溃睹。然后而账,
創(chuàng)建了 Person 的兩個實例。接著因篇,修改了 person1.friends 引用的數(shù)組泞辐,向數(shù)組中添加了一個字符
串。由于 friends 數(shù)組存在于 Person.prototype 而非 person1 中竞滓,所以剛剛提到的修改也會通過
person2.friends(與 person1.friends 指向同一個數(shù)組)反映出來咐吼。假如我們的初衷就是像這樣
在所有實例中共享一個數(shù)組,那么對這個結(jié)果我沒有話可說商佑【馇眩可是,實例一般都是要有屬于自己的全部
屬性的茶没。而這個問題正是我們很少看到有人單獨使用原型模式的原因所在肌幽。