JavaScript之創建對象的模式

JavaScript之創建對象的模式,使用Object的構造函數可以創建對象或者使用對象字面量來創建單個對象,但是這些方法有一個明顯的缺點:使用相同的一個接口創建很多對象,會產生大量的重復代碼。
(一)工廠模式
這種模式抽象瞭創建具體對象的過程。考慮到在ECMAScript中無法創建類,開發人員就開發瞭一種函數,用函數來封裝以特定接口創建對象的細節:



<script type=”text/javascript”>

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”);

person1.sayName(); //”Nicholas”
person2.sayName(); //”Greg”
</script>

工廠模式雖然解決瞭創建多個相似對象的問題,但是卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。

(二)構造函數模式

ECMAScript中的構造函數可以用來創建特定類型的對象。像Object和Array這樣的原生構造函數,在運行時會自動出現在執行環境中。此外,也可以創建自定義的構造函數,從而定義自定義對象類型的屬性和方法。例如,使用構造函數的方法重寫上訴問題:



<script type=”text/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”);

person1.sayName(); //”Nicholas”
person2.sayName(); //”Greg”

alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true

alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true

alert(person1.sayName == person2.sayName); //false

</script>

調用構造函數實際是經歷如下的過程:
(1)創建一個對象;
(2)將構造函數的作用域賦給新對象(因此this就指向瞭這個新對象);
(3)執行構造函數中的代碼(為新對象添加屬性);
(4)返回新對象;

使用這種模式,person1和person2對象分別保存著Person的一個不同的實例。這兩個對象都有一個constructor(構造函數)屬性,該屬性指向Person:
(函數名實際是個指針)

alert(person1.constructor == Person);  //true
alert(person2.constructor == Person);  //true

constructor屬性最初是用來標識對象類型的。以上創建的兩個對象即是Object的實例,同時也是Person(可以 person1 instanceof Person 來進行判斷)的實例(因為所有對象都繼承自Object),也就是說使用構造函數模式可以將它的實例標識為一種特定的類型,這也是構造函數模式勝過工廠模式的地方。
構造函數與其他函數的唯一區別就是在於它們的調用方式不同。任何函數,隻要通過new操作符來調用,那它就可以作為構造函數。同理,前面的Person函數可以通過以下任意一種方式調用:

        var person = new Person("Nicholas", 29, "Software Engineer");
        person.sayName();   //"Nicholas"

        Person("Greg", 27, "Doctor");  //adds to window
        window.sayName();   //"Greg"
        //使用call()(或者apply())方法在某個特殊的作用域中進行調用,此處是在對象o的作用域中進行調用,調用後o就擁有瞭所有屬性和方法   
        var o = new Object();
        Person.call(o, "Kristen", 25, "Nurse");
        o.sayName();    //"Kristen"

不使用new操作符的調用,屬性和方法都被添加給window對象。
有時構造函數可以這樣定義:

function Person(name, age, job){
            this.name = name;
            this.age = age;
            this.job = job;
            this.sayName = new function("alert(this.name)");  
        }

從這個角度看,每個Person實例都包含瞭一個不同的Function實例(以顯示name屬性)的本質。

alert(person1.sayName == person2.sayName); //false

然而創建兩個完成同樣任務的Function實例沒有必要,因此可以將函數定義轉移到構造函數外:

        function Person(name, age, job){
            this.name = name;
            this.age = age;
            this.job = job;
            this.sayName = sayName;
        }
        //sayName包含的是一個指向函數的指針
        function sayName(){
            alert(this.name);
        }

因此person1和person2對象就共享瞭在全局作用域中定義的一個sayName()函數。但是全局作用域中定義的函數隻能被某個對象調用,並且如果對象需要定義很多方法,那麼就需要多個全局變量函數,即沒有瞭封裝性

(三)原型模式
我們創建的每個函數都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象,它的用途是包含可以有特定類型的所有實例共享的屬性和方法。

prototype就是通過構造函數而創建的那個對象的原型對象。使用原型的好處就是可以讓所有對象實例共享它所包含的屬性和方法 。

function Person() {
}
Person.prototype.name = "zxj";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    alert(this.name);
}
var person1 = new Person();
person1.sayName(); //zxj
var person2 = new Person();
person2.sayName(); //zxj

(1)理解原型對象

無論什麼時候,隻要創建瞭一個新函數,ECMAScript就會根據一組特定的規則為該函數創建一個prototype屬性,這個屬性指向函數的原型對象。在默認情況下,所有原型對象都會自動獲得一個constructor(構造函數)屬性,這個屬性包含一個指向prototype屬性所在函數的指針。就拿前面的例子,Person.prototype.constructor指向Person。而通過這個構造函數,我們還可以繼續為原型對象添加其他屬性和方法。
創建瞭自定義指針之後,其原型對象默認隻會取得constructor屬性;至於其它方法,都會從Object對象繼承而來。當調用構造函數創建一個新實例之後,該實例的內部將包括一個指針(內部屬性),指向構造函數的原型對象。ECMA-262第5版中管這個叫[[Prototype]]。
要明確一點的就是,這個連接存在於實例和構造函數的原型對象之間,而不是存在於實例和構造函數之間。
以前面使用的Person構造函數和Person.prototype創建實例的代碼為例,如下圖:

這裡寫圖片描述

上圖展示瞭Person構造函數、Person的原型屬性以及Person現有的兩個實例之間的關系。在此,Person.prototype指向瞭原型對象,而Person.prototype.constructZ喎?/kf/ware/vc/” target=”_blank” class=”keylink”>vctPW1ri72MHLUGVyc29uoaPUrdDNttTP89bQs/3By7D8uqxjb25zdHJ1Y3Rvcsr00NTWrs3io6y7ubD8wKi688C0zO2807XExuTL+8r00NSho1BlcnNvbrXEw7/Su7j2yrXA/SZtZGFzaDsmbWRhc2g7cGVyc29uMbrNcGVyc29uMra8sPy6rNK7uPbE2rK/yvTQ1KOsuMPK9NDUvfa99ta4z/JQZXJzb24ucHJvdG90eXBloaO7u77ku7DLtaOsy/zDx9PrubnU7Lqvyv3Du9PQ1rG907XEwarPtaGjtMvN4qOs0qq48c3i16LS4rXEysejrMvkyLvV4sG9uPbKtcD9tryyu7D8uqzK9NDUus23vbeoo6y1q87Sw8fItL/J0tS199PDcGVyc29uMS5zYXlOYW1lKCmho9XiysfNqLn9sunV0rbUz/PK9NDUtcS5/bPMwLTKtc/WtcShozxiciAvPg0Ky+TIu87Sw8fO3reot8POyrW9W1tQcm90b3R5cGVdXaOstau/ydLUzai5/WlzUHJvdG90eXBlT2YoKbe9t6jAtMi3tqi21M/z1q685MrHt/G05tTa1eLW1rnYz7Who7TTsb7WysnPvbKjrMjnuftbW1Byb3RvdHlwZV1d1rjP8rX308Npc1Byb3RvdHlwZU9mKCm3vbeotcS21M/zo6hQZXJzb24ucHJvdG90eXBlo6ksxMfDtNXiuPa3vbeovs274be1u9h0cnVloaM8L3A+DQo8cHJlIGNsYXNzPQ==”brush:java;”>
alert(Person.prorotype.isPrototypeOf(person1)); //true
alert(Person.prorotype.isPrototypeOf(person2)); //true

每當代碼要讀取某個對象的屬性時,都會進行一次搜索,搜索目標是具有給定名稱的屬性。搜索當然先從對象實例的本身開始,如果找到瞭,就可以返回該值瞭;如果找不到,則會去指針所指向的原型對象中去查找,在原型對象中找到瞭,就可以順利返回該值。而這正是多個對象實例共享原型所保存的屬性和方法的基本原理。
雖然可以通過對象實例訪問到保存在原型中的值,但不能通過對象實例重寫原型中的值。根據查找原理,如果找到瞭實例中的值,就不會再去查找原型對象中的值。,代碼如下所示:

function Person() {
}
Person.prototype.name = "zxj";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.sayName = function () {
    alert(this.name);
}
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name); //Greg 來自實例
alert(person2.name); //zxj  來自原型
delete person1.name; //刪除實例中的name屬性
alert(person1.name); //zxj 來自原型

使用hasOwnPeoperty()方法可以檢測一個屬性是否存在於實例中,還是存在原型中,這個方法(它是從Object繼承來的)隻在給定屬性存在域對象實例中時,才返回true。

function Person() {
}
Person.prototype.name = "zxj";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.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); //zxj  來自原型
alert(person2.hasOwnProperty("name")); //false
delete person1.name;
alert(person1.name); //zxj 來自原型
alert(person1.hasOwnProperty("name")); //false

(2)原型與in操作符

有兩種方式使用in操作符:一、單獨使用;二、for-in中使用。
功能:會在通過對象能夠訪問給定屬性時返回true,無論是在對象實例中或是原型中。

function Person() {
}
Person.prototype.name = "zxj";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.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); //zxj  來自原型
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2);  //true

delete person1.name;
alert(person1.name); //zxj 來自原型
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1);  //true

alert(person1.hasOwnProperty("qqqq")); //false
alert("qqqq" in person1);  //false

同時使用hasOwnProperty()和in操作符可以判斷出該屬性到底是存在對象實例中還是存在與原型中。
使用for-in循環時,返回的是所有能夠通過對象訪問的,可枚舉(enumerated)屬性,其中即包括存在與實例中的屬性,也包括存在與原型中的屬性。根據規定,開發人員定義的屬性都是可枚舉的——IE8及更早版本除。

var o = {
    toString: function () {
        return "My Object";
    }
}
for (var prop in o) {
    if (prop == "toString") {
        alert("Found toString"); //在IE中不會顯示(IE9(未測試)和IE10(已測試)可用)
    }
}

(3)更簡單的原型語法

function Person(){
}

Person.peototype={
    name:"zxj",
    age:29,
    job:"Software Engineer",
    sayName:function(){
        alert(this.name);
    }
};

結果是與先前的相同,但有一個是不同的:contrcutor屬性不再指向Person瞭。我們曾經介紹過,沒創建一個函數,就會同時創建它的prototype對象,這個對象也會自動獲得constructor屬性。而我們這樣寫,本質上是完全重寫瞭默認的prototype對象,因此constructor屬性也就變成瞭新對象的constructor屬性(指向Object構造函數),不再指向Person函數。
當然我可以將它特意設置成適當的值:

function Person() {
}

Person.prototype = {
    constructor: Person,
    name: "zxj",
    age: 29,
    sayName: function () {
        alert(this.name);
    }
};

以上代碼特意包含瞭一個constructor屬性,並將它的值設置為Person,從而確保瞭通過該屬性能夠訪問到適當的值。
(4)原型的動態性

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

function Person() {
}

var friend = new Person();

Person.prototype.sayHi = function () {
    alert("hi");
}

friend.sayHi(); //"hi"

盡管可以隨時為原型添加屬性和方法,但如果我們重寫瞭整個原型對象,那麼情況就不一樣瞭。我們知道,調用構造函數時會為實例添加一個指向最初原型的[[Prototype]]指針,而把原型修改為另一個對象就等於切斷瞭構造函數與最初原型之間的聯系。一定要記住:實例中的指針僅僅指向原型,而不是構造函數。如下例子:

function Person() {
}

var friend = new Person();

Person.prototype = {
    constructor: Person,
    name: "zxj",
    age: 29,
    job: "Software Engineer",
    sayName: function () {
        alert(this.name);
    }
};

friend.sayName();   //error 找不到該方法

這裡寫圖片描述

(6)原型對象的問題

原型模式的最大問題是由其共享的本性所導致的。原型中的所有屬性都被很多實例共享的,這種共享對於函數非常合適。對於那些包含基本值的屬性,通過實例上添加一個同名屬性,可以隱藏原型中的對應屬性。然而,對於包含引用類型值的屬性來書,問題便比較突出:

Person.prototype = {
    constructor: Person,
    name: "zxj",
    age: 29,
    job: "Software Engineer",
    friends: ["saly", "geil"],
    sayName: function () {
        alert(this.name);
    }
};

var person1 = new Person();
var person2 = new Person();

person1.friends.push("van");

alert(person1.friends);  //"saly","geil","van"
alert(person2.friends);  //"saly","geil","van"

alert(person1.friends === person2.friends)  //問題出來瞭,person1結交瞭新朋友意味著person2也必須結交這個朋友

當一個對象想獲取獨有的操作時,原型模式的共享就是最大的阻礙。因此,目前使用最廣泛的是構造函數和原型混成的模式,構造函數模式用於定義實例屬性,而原型模式用於定於方法和共享屬性。

發佈留言