javascript設計模式介紹(三) 原型模式 擴展知識

原型與 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中不會顯示
        }
}


要取得對象上所有可枚舉的實例屬性,可以使用 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+、Opera 12+和Chrome。

更簡單的原型語法

前面例子中每添加一個屬性和方法就要敲一遍 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(friendinstanceof Object); //true
alert(friendinstanceof 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
});


 原型的動態性

由於在原型中查找值的過程是一次搜索,因此我們對原型對象所做的任何修改都能夠立即從實例上反映出來——即使是先創建了實例後修改原型也照樣如此。請看下面的例子。

<pre name="code" class="javascript">var friend = new Person();
Person.prototype.sayHi= function(){
        alert("hi");
};


friend.sayHi();//"hi"(沒有問題!)



以上代碼先創建了 Person 的一個實例,並將其保存在 friend 中。然後,下一條語句在 Person. prototype 中添加了一個方法 sayHi()。即使 friend 實例是在添加新方法之前創建的,但它仍然可以訪問這個新方法。其原因可以歸結爲實例與原型之間的鬆散連接關係。當我們調用 friend.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可以看出,重寫原型對象切斷了現有原型與任何之前已經存在的對象實例之間的聯繫;它們引用的仍然是最初的原型。

原生對象的原型

原型模式的重要性不僅體現在創建自定義類型方面,就連所有原生的引用類型,都是採用這種模式創建的。所有原生引用類型(Object、Array、String,等等)都在其構造函數的原型上定義了方法。例如,在 Array.prototype 中可以找到 sort()方法,而在 String.prototype 中可以找到substring()方法,如下所示。

alert(typeofArray.prototype.sort); //"function"
alert(typeofString.prototype.substring); //"function"


通過原生對象的原型,不僅可以取得所有默認方法的引用,而且也可以定義新方法。可以像修改自定義對象的原型一樣修改原生對象的原型,因此可以隨時添加方法。下面的代碼就給基本包裝類型String 添加了一個名爲 startsWith()的方法。

String.prototype.startsWith= function (text) {
        returnthis.indexOf(text) == 0;
};
 
varmsg = "Hello world!";
alert(msg.startsWith("Hello"));//true


這裏新定義的startsWith()方法會在傳入的文本位於一個字符串開始時返回 true。既然方法被添加給了 String.prototype,那麼當前環境中的所有字符串就都可以調用它。由於 msg 是字符串,而且後臺會調用String 基本包裝函數創建這個字符串,因此通過msg 就可以調用startsWith()方法。

儘管可以這樣做,但我們不推薦在產品化的程序中修改原生對象的原型。如果因某個實現中缺少某個方法,就在原生對象的原型中添加這個方法,那麼當在另一個支

持該方法的實現中運行代碼時,就可能會導致命名衝突。而且,這樣做也可能會意外地重寫原生方法。


原型對象的問題

原型模式也不是沒有缺點。首先,它省略了爲構造函數傳遞初始化參數這一環節,結果所有實例在默認情況下都將取得相同的屬性值。雖然這會在某種程度上帶來一些不方便,但還不是原型的最大問題。

原型模式的最大問題是由其共享的本性所導致的。原型中所有屬性是被很多實例共享的,這種共享對於函數非常合適。對於那些包含基本值的屬性倒也說得過去,畢竟(如前面的例子所示),通過在實例上添加一個同名屬性,可以隱藏原型中的對應屬性。然而,對於包含引用類型值的屬性來說,問題就比較突出了。來看下面的例子。

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指向同一個數組)反映出來。假如我們的初衷就是像這樣在所有實例中共享一個數組,那麼對這個結果沒有話可說。可是,實例一般都是要有屬於自己的全部屬性的。而這個問題正是我們很少看到有人單獨使用原型模式的原因所在。






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