雖然Object 構造函數或對象字面量均可以用來建立單個對象,但這些方式有個明顯的缺點:使用同一個接口建立不少對象,會產生大量的重複代碼。爲解決這個問題,人們開始使用工廠模式的一種變體。設計模式
工廠模式
工廠模式是軟件工程領域一種廣爲人知的設計模式,這種模式抽象了建立具體對象的過程(本書後面還將討論其餘設計模式及其在JavaScript 中的實現)。考慮到在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("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");數組
函數createPerson()可以根據接受的參數來構建一個包含全部必要信息的Person 對象。能夠無數次地調用這個函數,而每次它都會返回一個包含三個屬性一個方法的對象。工廠模式雖然解決了建立多個類似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。隨着JavaScript的發展,又一個新模式出現了。瀏覽器
構造函數模式
前幾章介紹過,ECMAScript 中的構造函數可用來建立特定類型的對象。像Object 和Array 這樣的原生構造函數,在運行時會自動出如今執行環境中。此外,也能夠建立自定義的構造函數,從而定義自定義對象類型的屬性和方法。例如,可使用構造函數模式將前面的例子重寫以下。安全
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");app
在這個例子中,Person()函數取代了createPerson()函數。咱們注意到,Person()中的代碼除了與createPerson()中相同的部分外,還存在如下不一樣之處:
沒有顯式地建立對象;
直接將屬性和方法賦給了this 對象;
沒有return 語句。
此外,還應該注意到函數名Person 使用的是大寫字母P。按照慣例,構造函數始終都應該以一個大寫字母開頭,而非構造函數則應該以一個小寫字母開頭。這個作法借鑑自其餘OO 語言,主要是爲了區別於ECMAScript 中的其餘函數;由於構造函數自己也是函數,只不過能夠用來建立對象而已。要建立Person 的新實例,必須使用new 操做符。以這種方式調用構造函數實際上會經歷如下4個步驟:
(1) 建立一個新對象;
(2) 將構造函數的做用域賦給新對象(所以this 就指向了這個新對象);
(3) 執行構造函數中的代碼(爲這個新對象添加屬性);
(4) 返回新對象。
在前面例子的最後,person1 和person2 分別保存着Person 的一個不一樣的實例。這兩個對象都有一個constructor(構造函數)屬性,該屬性指向Person,以下所示。
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
對象的constructor 屬性最初是用來標識對象類型的。可是,提到檢測對象類型,仍是instanceof操做符要更可靠一些。咱們在這個例子中建立的全部對象既是Object 的實例,同時也是Person的實例,這一點經過instanceof 操做符能夠獲得驗證。函數
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
建立自定義的構造函數意味着未來能夠將它的實例標識爲一種特定的類型;而這正是構造函數模式賽過工廠模式的地方。在這個例子中,person1 和person2 之因此同時是Object 的實例,是由於全部對象均繼承自Object(詳細內容稍後討論)。性能
注:以這種方式定義的構造函數是定義在Global 對象(在瀏覽器中是window 對象)中的。第8 章將詳細討論瀏覽器對象模型(BOM)。測試
1. 將構造函數看成函數
構造函數與其餘函數的惟一區別,就在於調用它們的方式不一樣。不過,構造函數畢竟也是函數,不存在定義構造函數的特殊語法。任何函數,只要經過new 操做符來調用,那它就能夠做爲構造函數;而任何函數,若是不經過new 操做符來調用,那它跟普通函數也不會有什麼兩樣。例如,前面例子中定義的Person()函數能夠經過下列任何一種方式來調用。
// 看成構造函數使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"
// 做爲普通函數調用
Person("Greg", 27, "Doctor"); // 添加到window
window.sayName(); //"Greg"
// 在另外一個對象的做用域中調用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"this
這個例子中的前兩行代碼展現了構造函數的典型用法,即便用new 操做符來建立一個新對象。接下來的兩行代碼展現了不使用new 操做符調用Person()會出現什麼結果:屬性和方法都被添加給window對象了。有讀者可能還記得,當在全局做用域中調用一個函數時,this 對象老是指向Global 對象(在瀏覽器中就是window 對象)。所以,在調用完函數以後,能夠經過window 對象來調用sayName()方法,而且還返回了"Greg"。最後,也可使用call()(或者apply())在某個特殊對象的做用域中調用Person()函數。這裏是在對象o 的做用域中調用的,所以調用後o 就擁有了全部屬性和sayName()方法。google
2. 構造函數的問題
構造函數模式雖然好用,但也並不是沒有缺點。使用構造函數的主要問題,就是每一個方法都要在每一個實例上從新建立一遍。在前面的例子中,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 實例(以顯示name 屬性)的本質。說明白些,以這種方式建立函數,會致使不一樣的做用域鏈和標識符解析,但建立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);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
在這個例子中,咱們把sayName()函數的定義轉移到了構造函數外部。而在構造函數內部,咱們將sayName 屬性設置成等於全局的sayName 函數。這樣一來,因爲sayName 包含的是一個指向函數的指針,所以person1 和person2 對象就共享了在全局做用域中定義的同一個sayName()函數。這樣作確實解決了兩個函數作同一件事的問題,但是新問題又來了:在全局做用域中定義的函數實際上只能被某個對象調用,這讓全局做用域有點名存實亡。而更讓人沒法接受的是:若是對象須要定義不少方法,那麼就要定義不少個全局函數,因而咱們這個自定義的引用類型就絲毫沒有封裝性可言了。好在,這些問題能夠經過使用原型模式來解決。
原型模式
咱們建立的每一個函數都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法。若是按照字面意思來理解,那麼prototype 就是經過調用構造函數而建立的那個對象實例的原型對象。使用原型對象的好處是可讓全部對象實例共享它所包含的屬性和方法。換句話說,沒必要在構造函數中定義對象實例的信息,而是能夠將這些信息直接添加到原型對象中,以下面的例子所示。
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 屬性中,構造函數變成了空函數。即便如此,也仍然能夠經過調用構造函數來建立新對象,並且新對象還會具備相同的屬性和方法。但與構造函數模式不一樣的是,新對象的這些屬性和方法是由全部實例共享的。換句話說,person1 和person2 訪問的都是同一組屬性和同一個sayName()函數。要理解原型模式的工做原理,必須先理解ECMAScript 中原型對象的性質。
1. 理解原型對象
不管何時,只要建立了一個新函數,就會根據一組特定的規則爲該函數建立一個prototype屬性,這個屬性指向函數的原型對象。在默認狀況下,全部原型對象都會自動得到一個constructor(構造函數)屬性,這個屬性包含一個指向prototype 屬性所在函數的指針。就拿前面的例子來講,Person.prototype. constructor 指向Person。而經過這個構造函數,咱們還可繼續爲原型對象添加其餘屬性和方法。
建立了自定義的構造函數以後,其原型對象默認只會取得constructor 屬性;至於其餘方法,則都是從Object 繼承而來的。當調用構造函數建立一個新實例後,該實例的內部將包含一個指針(內部屬性),指向構造函數的原型對象。ECMA-262 第5 版中管這個指針叫[[Prototype]]。雖然在腳本中沒有標準的方式訪問[[Prototype]],但Firefox、Safari 和Chrome 在每一個對象上都支持一個屬性__proto__;而在其餘實現中,這個屬性對腳本則是徹底不可見的。不過,要明確的真正重要的一點就是,這個鏈接存在於實例與構造函數的原型對象之間,而不是存在於實例與構造函數之間。
之前面使用Person 構造函數和Person.prototype 建立實例的代碼爲例,圖6-1 展現了各個對象之間的關係。
圖6-1 展現了Person 構造函數、Person 的原型屬性以及Person 現有的兩個實例之間的關係。
在此,Person.prototype 指向了原型對象,而Person.prototype.constructor 又指回了Person。
原型對象中除了包含constructor 屬性以外,還包括後來添加的其餘屬性。Person 的每一個實例——person1 和person2 都包含一個內部屬性,該屬性僅僅指向了Person.prototype;換句話說,它們與構造函數沒有直接的關係。此外,要格外注意的是,雖然這兩個實例都不包含屬性和方法,但咱們卻能夠調用person1.sayName()。這是經過查找對象屬性的過程來實現的。
雖然在全部實現中都沒法訪問到[[Prototype]],但能夠經過isPrototypeOf()方法來肯定對象之間是否存在這種關係。從本質上講,若是[[Prototype]]指向調用isPrototypeOf()方法的對象(Person.prototype),那麼這個方法就返回true,以下所示:
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
這裏,咱們用原型對象的isPrototypeOf()方法測試了person1 和person2。由於它們內部都有一個指向Person.prototype 的指針,所以都返回了true。
ECMAScript 5 增長了一個新方法,叫Object.getPrototypeOf(),在全部支持的實現中,這個方法返回[[Prototype]]的值。例如:
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"
這裏的第一行代碼只是肯定Object.getPrototypeOf()返回的對象實際就是這個對象的原型。
第二行代碼取得了原型對象中name 屬性的值,也就是"Nicholas"。使用Object.getPrototypeOf()能夠方便地取得一個對象的原型,而這在利用原型實現繼承(本章稍後會討論)的狀況下是很是重要的。
支持這個方法的瀏覽器有IE9+、Firefox 3.5+、Safari 5+、Opera 12+和Chrome。
每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具備給定名字的屬性。搜索首先從對象實例自己開始。若是在實例中找到了具備給定名字的屬性,則返回該屬性的值;若是沒有找到,則繼續搜索指針指向的原型對象,在原型對象中查找具備給定名字的屬性。若是在原型對象中找到了這個屬性,則返回該屬性的值。也就是說,在咱們調用person1.sayName()的時候,會前後執行兩次搜索。首先,解析器會問:「實例person1 有sayName 屬性嗎?」答:「沒有。」而後,它繼續搜索,再問:「person1 的原型有sayName 屬性嗎?」答:「有。」因而,它就讀取那個保存在原型對象中的函數。當咱們調用person2.sayName()時,將會重現相同的搜索過程,獲得相同的結果。而這正是多個對象實例共享原型所保存的屬性和方法的基本原理。
注:前面提到過,原型最初只包含constructor 屬性,而該屬性也是共享的,所以能夠經過對象實例訪問。
雖然能夠經過對象實例訪問保存在原型中的值,但卻不能經過對象實例重寫原型中的值。若是咱們在實例中添加了一個屬性,而該屬性與實例原型中的一個屬性同名,那咱們就在實例中建立該屬性,該屬性將會屏蔽原型中的那個屬性。來看下面的例子。
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"(來自原型)。
當在alert()中訪問person1.name 時,須要讀取它的值,所以就會在這個實例上搜索一個名爲name的屬性。這個屬性確實存在,因而就返回它的值而沒必要再搜索原型了。當以一樣的方式訪問person2.name 時,並無在實例上發現該屬性,所以就會繼續搜索原型,結果在那裏找到了name 屬性。
當爲對象實例添加一個屬性時,這個屬性就會屏蔽原型對象中保存的同名屬性;換句話說,添加這個屬性只會阻止咱們訪問原型中的那個屬性,但不會修改那個屬性。即便將這個屬性設置爲null,也只會在實例中設置這個屬性,而不會恢復其指向原型的鏈接。不過,使用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"值屏蔽了同名的原型屬性。把它刪除之後,就恢復了對原型中name 屬性的鏈接。所以,接下來再調用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()方法,何時訪問的是實例屬性,何時訪問的是原型屬性就一清二楚了。調用person1.hasOwnProperty( "name")時,只有當person1 重寫name 屬性後纔會返回true,由於只有這時候name 纔是一個實例屬性,而非原型屬性。圖6-2 展現了上面例子在不一樣狀況下的實現與原型的關係(爲了簡單起見,圖中省略了與Person 構造函數的關係)。
注:ECMAScript 5 的Object.getOwnPropertyDescriptor()方法只能用於實例屬性,要取得原型屬性的描述符,必須直接在原型對象上調用Object.getOwnProperty-Descriptor()方法。
2. 原型與in 操做符
有兩種方式使用in 操做符:單獨使用和在for-in 循環中使用。在單獨使用時,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
在以上代碼執行的整個過程當中,name 屬性要麼是直接在對象上訪問到的,要麼是經過原型訪問到的。所以,調用"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,就能夠肯定屬性是原型中的屬性。下面來看一看上面定義的函數hasPrototypeProperty()的用法。
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person = new Person();
alert(hasPrototypeProperty(person, "name")); //true
person.name = "Greg";
alert(hasPrototypeProperty(person, "name")); //false
在這裏,name 屬性先是存在於原型中,所以hasPrototypeProperty()返回true。當在實例中重寫name 屬性後,該屬性就存在於實例中了,所以hasPrototypeProperty()返回false。即便原型中仍然有name 屬性,但因爲如今實例中也有了這個屬性,所以原型中的name 屬性就用不到了。
在使用for-in 循環時,返回的是全部可以經過對象訪問的、可枚舉的(enumerated)屬性,其中既包括存在於實例中的屬性,也包括存在於原型中的屬性。屏蔽了原型中不可枚舉屬性(即將[[Enumerable]]標記爲false 的屬性)的實例屬性也會在for-in 循環中返回,由於根據規定,全部開發人員定義的屬性都是可枚舉的——只有在IE8 及更早版本中例外。
IE 早期版本的實現中存在一個bug,即屏蔽不可枚舉屬性的實例屬性不會出如今for-in 循環中。
例如:
var o = {
toString : function(){
return "My Object";
}
};
for (var prop in o){
if (prop == "toString"){
alert("Found toString"); //在IE 中不會顯示
}
}
當以上代碼運行時,應該會顯示一個警告框,代表找到了toString()方法。這裏的對象o 定義了一個名爲toString()的方法,該方法屏蔽了原型中(不可枚舉)的toString()方法。在IE 中,因爲其實現認爲原型的toString()方法被打上了值爲false 的[[Enumerable]]標記,所以應該跳過該屬性,結果咱們就不會看到警告框。該bug 會影響默認不可枚舉的全部屬性和方法,包括:hasOwnProperty()、propertyIsEnumerable()、toLocaleString()、toString()和valueOf()。ECMAScript 5 也將constructor 和prototype 屬性的[[Enumerable]]特性設置爲false,但並非全部瀏覽器都照此實現。
要取得對象上全部可枚舉的實例屬性,可使用ECMAScript 5 的Object.keys()方法。這個方法接收一個對象做爲參數,返回一個包含全部可枚舉屬性的字符串數組。例如:
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 中將保存一個數組,數組中是字符串"name"、"age"、"job"和"sayName"。這個順序也是它們在for-in 循環中出現的順序。若是是經過Person 的實例調用,則Object.keys()返回的數組只包含"name"和"age"這兩個實例屬性。
若是你想要獲得全部實例屬性,不管它是否可枚舉,均可以使用Object.getOwnPropertyNames()方法。
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); //"constructor,name,age,job,sayName"
注意結果中包含了不可枚舉的constructor 屬性。Object.keys()和Object.getOwnProperty-Names()方法均可以用來替代for-in 循環。支持這兩個方法的瀏覽器有IE9+、Firefox 4+、Safari 5+、Opera12+和Chrome。
3. 更簡單的原型語法
讀者大概注意到了,前面例子中每添加一個屬性和方法就要敲一遍Person.prototype。爲減小沒必要要的輸入,也爲了從視覺上更好地封裝原型的功能,更常見的作法是用一個包含全部屬性和方法的對象字面量來重寫整個原型對象,以下面的例子所示。
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
在上面的代碼中,咱們將Person.prototype 設置爲等於一個以對象字面量形式建立的新對象。最終結果相同,但有一個例外:constructor 屬性再也不指向Person 了。前面曾經介紹過,每建立一個函數,就會同時建立它的prototype 對象,這個對象也會自動得到constructor 屬性。而咱們在這裏使用的語法,本質上徹底重寫了默認的prototype 對象,所以constructor 屬性也就變成了新對象的constructor 屬性(指向Object 構造函數),再也不指向Person 函數。此時,儘管instanceof操做符還能返回正確的結果,但經過constructor 已經沒法肯定對象的類型了,以下所示。
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 的值真的很重要,能夠像下面這樣特地將它設置回適當的值。
function Person(){
}
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
以上代碼特地包含了一個constructor 屬性,並將它的值設置爲Person,從而確保了經過該屬性可以訪問到適當的值。
注意,以這種方式重設constructor 屬性會致使它的[[Enumerable]]特性被設置爲true。默認狀況下,原生的constructor 屬性是不可枚舉的,所以若是你使用兼容ECMAScript 5 的JavaScript 引擎,能夠試一試Object.defineProperty()。
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
//重設構造函數,只適用於ECMAScript 5 兼容的瀏覽器
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
4. 原型的動態性
因爲在原型中查找值的過程是一次搜索,所以咱們對原型對象所作的任何修改都可以當即從實例上反映出來——即便是先建立了實例後修改原型也照樣如此。請看下面的例子。
var friend = new Person();
Person.prototype.sayHi = function(){
alert("hi");
};
friend.sayHi(); //"hi"(沒有問題!)
以上代碼先建立了Person 的一個實例,並將其保存在person 中。而後,下一條語句在Person.prototype 中添加了一個方法sayHi()。即便person 實例是在添加新方法以前建立的,但它仍然能夠訪問這個新方法。其緣由能夠歸結爲實例與原型之間的鬆散鏈接關係。當咱們調用person.sayHi()時,首先會在實例中搜索名爲sayHi 的屬性,在沒找到的狀況下,會繼續搜索原型。由於實例與原型之間的鏈接只不過是一個指針,而非一個副本,所以就能夠在原型中找到新的sayHi 屬性並返回保存在那裏的函數。
儘管能夠隨時爲原型添加屬性和方法,而且修改可以當即在全部對象實例中反映出來,但若是是重寫整個原型對象,那麼狀況就不同了。咱們知道,調用構造函數時會爲實例添加一個指向最初原型的[[Prototype]]指針,而把原型修改成另一個對象就等於切斷了構造函數與最初原型之間的聯繫。
請記住:實例中的指針僅指向原型,而不指向構造函數。看下面的例子。
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
在這個例子中,咱們先建立了Person 的一個實例,而後又重寫了其原型對象。而後在調用friend.sayName()時發生了錯誤,由於friend 指向的原型中不包含以該名字命名的屬性。圖6-3 展現了這個過程的內幕。
從圖6-3 能夠看出,重寫原型對象切斷了現有原型與任何以前已經存在的對象實例之間的聯繫;它們引用的仍然是最初的原型。
5. 原生對象的原型
原型模式的重要性不只體如今建立自定義類型方面,就連全部原生的引用類型,都是採用這種模式建立的。全部原生引用類型(Object、Array、String,等等)都在其構造函數的原型上定義了方法。
例如,在Array.prototype 中能夠找到sort()方法,而在String.prototype 中能夠找到substring()方法,以下所示。
alert(typeof Array.prototype.sort); //"function"
alert(typeof String.prototype.substring); //"function"
經過原生對象的原型,不只能夠取得全部默認方法的引用,並且也能夠定義新方法。能夠像修改自定義對象的原型同樣修改原生對象的原型,所以能夠隨時添加方法。下面的代碼就給基本包裝類型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,那麼當前環境中的全部字符串就均可以調用它。因爲msg 是字符串,並且後臺會調用String 基本包裝函數建立這個字符串,所以經過msg 就能夠調用startsWith()方法。
注:儘管能夠這樣作,但咱們不推薦在產品化的程序中修改原生對象的原型。若是因某個實現中缺乏某個方法,就在原生對象的原型中添加這個方法,那麼當在另外一個支持該方法的實現中運行代碼時,就可能會致使命名衝突。並且,這樣作也可能會意外地重寫原生方法。
6. 原型對象的問題
原型模式也不是沒有缺點。首先,它省略了爲構造函數傳遞初始化參數這一環節,結果全部實例在默認狀況下都將取得相同的屬性值。雖然這會在某種程度上帶來一些不方便,但還不是原型的最大問題。
原型模式的最大問題是由其共享的本性所致使的。
原型中全部屬性是被不少實例共享的,這種共享對於函數很是合適。對於那些包含基本值的屬性倒也說得過去,畢竟(如前面的例子所示),經過在實例上添加一個同名屬性,能夠隱藏原型中的對應屬性。然而,對於包含引用類型值的屬性來講,問題就比較突出了。來看下面的例子。
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 的屬性,該屬性包含一個字符串數組。而後,建立了Person 的兩個實例。接着,修改了person1.friends 引用的數組,向數組中添加了一個字符串。因爲friends 數組存在於Person.prototype 而非person1 中,因此剛剛提到的修改也會經過person2.friends(與person1.friends 指向同一個數組)反映出來。假如咱們的初衷就是像這樣在全部實例中共享一個數組,那麼對這個結果我沒有話可說。但是,實例通常都是要有屬於本身的所有屬性的。而這個問題正是咱們不多看到有人單獨使用原型模式的緣由所在。
組合使用構造函數模式和原型模式
建立自定義類型的最多見方式,就是組合使用構造函數模式與原型模式。構造函數模式用於定義實例屬性,而原型模式用於定義方法和共享的屬性。結果,每一個實例都會有本身的一份實例屬性的副本,但同時又共享着對方法的引用,最大限度地節省了內存。另外,這種混成模式還支持向構造函數傳遞參數;可謂是集兩種模式之長。下面的代碼重寫了前面的例子。
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("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
在這個例子中,實例屬性都是在構造函數中定義的,而由全部實例共享的屬性constructor 和方法sayName()則是在原型中定義的。而修改了person1.friends(向其中添加一個新字符串),並不會影響到person2.friends,由於它們分別引用了不一樣的數組。
這種構造函數與原型混成的模式,是目前在ECMAScript 中使用最普遍、認同度最高的一種建立自定義類型的方法。能夠說,這是用來定義引用類型的一種默認模式。
動態原型模式
有其餘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("Nicholas", 29, "Software Engineer");
friend.sayName();
注意構造函數代碼中加粗的部分。這裏只在sayName()方法不存在的狀況下,纔會將它添加到原型中。這段代碼只會在初次調用構造函數時纔會執行。此後,原型已經完成初始化,不須要再作什麼修改了。不過要記住,這裏對原型所作的修改,可以當即在全部實例中獲得反映。所以,這種方法確實能夠說很是完美。其中,if 語句檢查的能夠是初始化以後應該存在的任何屬性或方法——沒必要用一大堆if 語句檢查每一個屬性和每一個方法;只要檢查其中一個便可。對於採用這種模式建立的對象,還可使用instanceof 操做符肯定它的類型。
注:使用動態原型模式時,不能使用對象字面量重寫原型。前面已經解釋過了,若是在已經建立了實例的狀況下重寫原型,那麼就會切斷現有實例與新原型之間的聯繫。
寄生構造函數模式
一般,在前述的幾種模式都不適用的狀況下,可使用寄生(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("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
在這個例子中,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");
alert(colors.toPipedString()); //"red|blue|green"
在這個例子中,咱們建立了一個名叫SpecialArray 的構造函數。在這個函數內部,首先建立了一個數組,而後push()方法(用構造函數接收到的全部參數)初始化了數組的值。隨後,又給數組實例添加了一個toPipedString()方法,該方法返回以豎線分割的數組值。最後,將數組以函數值的形式返回。接着,咱們調用了SpecialArray 構造函數,向其中傳入了用於初始化數組的值,此後又調用了toPipedString()方法。
關於寄生構造函數模式,有一點須要說明:首先,返回的對象與構造函數或者與構造函數的原型屬性之間沒有關係;也就是說,構造函數返回的對象與在構造函數外部建立的對象沒有什麼不一樣。爲此,不能依賴instanceof 操做符來肯定對象類型。因爲存在上述問題,咱們建議在可使用其餘模式的狀況下,不要使用這種模式。
穩妥構造函數模式
道格拉斯·克羅克福德(Douglas Crockford)發明了JavaScript 中的穩妥對象(durable objects)這個概念。所謂穩妥對象,指的是沒有公共屬性,並且其方法也不引用this 的對象。穩妥對象最適合在一些安全的環境中(這些環境中會禁止使用this 和new),或者在防止數據被其餘應用程序(如Mashup程序)改動時使用。穩妥構造函數遵循與寄生構造函數相似的模式,但有兩點不一樣:一是新建立對象的實例方法不引用this;二是不使用new 操做符調用構造函數。按照穩妥構造函數的要求,能夠將前面的Person 構造函數重寫以下。
function Person(name, age, job){
//建立要返回的對象
var o = new Object();
//能夠在這裏定義私有變量和函數
//添加方法
o.sayName = function(){
alert(name);
};
//返回對象
return o;
}
注意,在以這種模式建立的對象中,除了使用sayName()方法以外,沒有其餘辦法訪問name 的值。
能夠像下面使用穩妥的Person 構造函數。
var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
這樣,變量friend 中保存的是一個穩妥對象,而除了調用sayName()方法外,沒有別的方式能夠訪問其數據成員。即便有其餘代碼會給這個對象添加方法或數據成員,但也不可能有別的辦法訪問傳入到構造函數中的原始數據。穩妥構造函數模式提供的這種安全性,使得它很是適合在某些安全執行環境——例如,ADsafe(www.adsafe.org)和Caja(http://code.google.com/p/google-caja/)提供的環境——下使用。
注:與寄生構造函數模式相似,使用穩妥構造函數模式建立的對象與構造函數之間也沒有什麼關係,所以instanceof 操做符對這種對象也沒有意義。