普通創建對象和字面量創建對象不足之處:
雖然 Object 構造函數或對象字面量都可以用來創建單個對象,但這些方式有個明顯的缺點:使用同一個接口創建很多對象,會產生大量的重復代碼。為解決這個問題, ** 我們采用其他方式創建對象 。**
1.方式(一):工廠模式####
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的發展,又一個新模式出現了。
方式(二):構造函數模式####
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");
解釋: 在這個例子中, Person()函數取代了 createPerson()函數。我們注意到, Person()中的代碼除了與 createPerson()中相同的部分外,還存在以下不同之處:
- 1.? 沒有顯式地創建對象;
- 2.? 直接將屬性和方法賦給了 this 對象;
- 3.? 沒有 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(詳細內容稍后討論)。
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"
ConstructorPatternExample02.htm
解釋:
-這個例子中的前兩行代碼展示了構造函數的典型用法,即使用 new 操作符來創建一個新對象。接下來的兩行代碼展示了不使用 new 操作符調用 Person()會出現什么結果:屬性和方法都被添加給 window對象了。有讀者可能還記得,當在全局作用域中調用一個函數時, this 對象總是指向 Global 對象(在瀏覽器中就是 window 對象)。因此,在調用完函數之后,可以通過 window 對象來調用 sayName()方法,并且還返回了"Greg"。最后,也可以使用 call()(或者 apply())在某個特殊對象的作用域中調用 Person()函數。這里是在對象 o 的作用域中調用的,因此調用后 o 就擁有了所有屬性和 sayName()方法。
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()時,將會重現相同的搜索過程,得到相同的結果。而這正是多個對象實例共享原型所保存的屬性和方法的基本原理。
雖然可以通過對象實例訪問保存在原型中的值,但卻不能通過對象實例重寫原型中的值。如果我們在實例中添加了一個屬性,而該屬性與實例原型中的一個屬性同名,那我們就在實例中創建該屬性,該屬性將會屏蔽原型中的那個屬性。來看下面的例子。
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.getOwnPropertyDescriptor()方法。
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 及更早版本中例外。
在使用 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.getOwnPropertyNames()方法都可以用來替代 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 操作符對這種對象也沒有意義。