javascript設計模式介紹(二) 構造函數模式

我們可以通過創建自定義的構造函數,從而定義自定義對象類型的屬性和方法。

例如:

function  Person(name.age,sex){
this.name = name;
this.age = age;
this.sex = sex;
this.sayName = function(){ alert(this.name); }
}
然後我們實例一個Person
var person1 = new Person("john",18,"男");
var person1 = new Person("Rose",17,"女");

我們注意到,Person()中的代碼:

 沒有顯式地創建對象; 

 直接將屬性和方法賦給了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 對象)中的。)

1.將構造函數當作函數 


構造函數與其他函數的唯一區別,就在於調用它們的方式不同。
不過,構造函數畢竟也是函數,不存在定義構造函數的特殊語法。任何函數,只要通過new 操作符來調用,那它就可以作爲構造函數;而任何函數,如果不通過new 操作符來調用,那它跟普通函數也不會有什麼兩樣。例如,前面例子中定義的Person()函數可以通過下列任何一種方式來調用。

 // 當作構造函數使用 
var person = new Person("Nicholas", 29, "man"); 
person.sayName(); //"Nicholas" 
// 作爲普通函數調用 
Person("Greg", 27, "women"); // 添加到window
window.sayName(); //"Greg" 
// 在另一個對象的作用域中調用 
var o = new Object(); 
Person.call(o, "Kristen", 25, "Nurse"); 
o.sayName(); //"Kristen"
這個例子中的前兩行代碼展示了構造函數的典型用法,即使用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, sex){ 
this.name = name;
this.age = age; 
this.sex= sex; 
this.sayName = sayName;
 } 
function sayName(){ 
alert(this.name);
 } 
var person1 = new Person("Nicholas", 29, "man");
var person2 = new Person("Greg", 27, "woman");

我們把sayName()函數的定義轉移到了構造函數外部。而在構造函數內部,我們將sayName 屬性設置成等於全局的sayName 函數。
這樣一來,由於sayName 包含的是一個指向函數的指針,因此person1 和person2 對象就共享了在全局作用域中定義的同一個sayName()函數。
這樣做確實解決了兩個函數做同一件事的問題,可是新問題又來了:在全局作用域中定義的函數實際上只能被某個對象調用,這讓全局作用域有點名不副實。
而更讓人無法接受的是:如果對象需要定義很多方法,那麼就要定義很多個全局函數,於是我們這個自定義的引用類型就絲毫沒有封裝性可言了。
 







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