javascript設計模式介紹(三) 原型模式

我們創建的每個函數都有一個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中原型對象的性質。


理解原型對象

無論什麼時候,只要創建了一個新函數,就會根據一組特定的規則爲該函數創建一個 prototype屬性,這個屬性指向函數的原型對象。在默認情況下,所有原型對象都會自動獲得一個 constructor(構造函數)屬性,這個屬性包含一個指向 prototype 屬性所在函數的指針。

就拿前面的例子來說,Person.prototype. constructor 指向 Person。

而通過這個構造函數,我們還可繼續爲原型對象添加其他屬性和方法。創建了自定義的構造函數之後,其原型對象默認只會取得 constructor 屬性;至於其他方法,則都是從 Object 繼承而來的。當調用構造函數創建一個新實例後,該實例的內部將包含一個指針(內部屬性),指向構造函數的原型對象。ECMA-262第 5版中管這個指針叫[[Prototype]]。雖然在腳本中沒有標準的方式訪問[[Prototype]],但 Firefox、Safari 和 Chrome 在每個對象上都支持一個屬性__proto__;而在其他實現中,這個屬性對腳本則是完全不可見的。不過,要明確的真正重要的一點就是,這個連接存在於實例與構造函數的原型對象之間,而不是存在於實例與構造函數之間。以前面使用 Person 構造函數和 Person.prototype 創建實例的代碼爲例,圖 6-1展示了各個對象之間的關係。

在此,Person.prototype指向了原型對象,而Person.prototype.constructor又指回了Person。原型對象中除了包含constructor 屬性之外,還包括後來添加的其他屬性。Person 的每個實例——person1 和 person2 都包含一個內部屬性,該屬性僅僅指向了 Person.prototype;換句話說,它們與構造函數沒有直接的關係。此外,要格外注意的是,雖然這兩個實例都不包含屬性和方法,但我們卻可以調用 person1.sayName()。這是通過查找對象屬性的過程來實現的。

雖然在所有實現中都無法訪問到[[Prototype]],但可以通過isPrototypeOf()方法來確定對象之間是否存在這種關係。從本質上講,如果[[Prototype]]指向調用 isPrototypeOf()方法的對象(Person.prototype),那麼這個方法就返回 true,如下所示:

<span style="white-space:pre">	</span>alert(Person.prototype.isPrototypeOf(person1));//true
	alert(Person.prototype.isPrototypeOf(person2));//true

這裏,我們用原型對象的 isPrototypeOf()方法測試了 person1 和 person2。因爲它們內部都有一個指向 Person.prototype 的指針,因此都返回了 true。

ECMAScript 5增加了一個新方法,叫Object.getPrototypeOf(),在所有支持的實現中,這個方法返回[[Prototype]]的值。例如:

<span style="white-space:pre">	</span>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"——來自原型
deleteperson1.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 構造函數的關係)。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章