在學習《JavaScript高級程序設計》(第3版)第六章創建對象時,遇到了針對創建自定義類型對象的幾種設計模式。其中的工廠模式與寄生構造函數模式以及穩妥構造函數模式三者在實現上十分相似,但卻具有微妙的差別,所以對它們做一個總結。
一、工廠模式
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");
工廠模式顧名思義,就是通過定義一個通用的函數,將對象的所有創建工作都封裝到這個函數中。之后每當需要創建一個對象時,只需要調用這個函數,同時給出初始化對象所需的各個參數,就能自動返回創建好的對象。這就如同工廠里批量生產一件件產品一般,因為創建出的所有對象之間雖然內容不同,但都出自同一模板。
二、寄生構造函數模式
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 person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
寄生構造函數模式與工廠模式極為相似,區別在于:
- 寄生構造函數模式將工廠模式中的那個通用函數
createPerson()
改名為Person()
,并將其看作為對象的構造函數。 - 創建對象實例時,寄生構造函數模式采用
new
操作符
那么兩者有什么功能上的差別呢?事實上,兩者本質上的差別僅在于new
操作符(因為函數取什么名字無關緊要),工廠模式創建對象時將createPerson
看作是普通的函數,而寄生構造函數模式創建對象時將Person
看作是構造函數,不過這對于創建出的對象來說,沒有任何差別。
對于兩者的差別,作者在書中是這么說的:
除了使用new操作符并把使用的包裝函數叫做構造函數之外,這個模式跟工廠模式其實是一模一樣的。構造函數在不返回值的情況下,默認會返回新對象實例。而通過在構造函數的末尾添加一個return語句,可以重寫調用構造函數時返回的值。
根據作者的意思,構造函數和普通函數的區別在于:當使用new
+構造函數創建對象時,如果構造函數內部沒有return
語句,那么默認情況下構造函數將返回一個該類型的實例(如果以上面的例子為參考,person1和person2為Person
類型的對象實例,可以使用person1 instanceof Person檢驗),但如果構造函數內部通過return
語句返回了一個其它類型的對象實例,那么這種默認的設置將被打破,構造函數最終返回的實例類型將以return
語句中對象實例的類型為準。
基于這個規則,在Person()
構造函數中,由于最后通過return
語句返回了一個Object
類型的對象實例,所以通過該構造函數創建的對象實際上是Object
類型而不是Person
類型;這樣一來就與createPerson()
函數返回的對象類型相同,因此可以說工廠模式和寄生構造函數模式在功能上是等價的。
如果非要說兩者的不同,并且要從其中選擇一個作為創建對象的方法的話,我個人更偏向于寄生構造函數模式一些。這是因為new Person()
(寄生構造函數模式)更能讓我感覺到自己正在創建一個對象,而不是在調用一個函數(工廠模式)。
三、穩妥構造函數模式
function Person(para_name, para_age, para_job) {
//創建要返回的對象
var o = {};
//在這里定義私有屬性和方法
var name = para_name;
var age = para_age;
var job = para_job;
var sayAge = function() {
alert(age);
};
//在這里定義公共方法
o.sayName = function() {
alert(name);
};
//返回對象
return o;
}
var person1 = Person("Nicholas", 29, "Software Engineer"); //創建對象實例
person1.sayName(); //Nicholas
person1.name; //undefined
person1.sayAge(); // 報錯
穩妥構造函數模式與前面介紹的兩種設計模式具有相似的地方,都是在函數內部定義好對象之后返回該對象來創建實例。然而穩妥構造函數模式的獨特之處在于具有以下特點:
- 沒有通過對象定義公共屬性
- 在公共方法中不使用this引用對象自身
- 不使用new操作符調用構造函數
這種設計模式最適合在一些安全的環境中使用(這些環境中會禁止使用this和new);為了較好地理解這種設計模式,我們可以采取類比的方法——這種構造對象的方式就如同C++/Java語言中通過訪問控制符private
定義出包含私有成員的類的方式一樣(將上例按C++中類的方式來定義):
class Person {
//定義私有成員變量和函數
private:
string name;
int age;
string job;
int sayAge() {return age;}
//定義構造函數和公共方法(函數)
public:
string sayName() {return name;} //公共方法
Person(string p_name, int p_age, string p_job):name(p_name),age(p_age),job(p_job) {} //構造函數
}
//創建對象實例
Person person1("Nicholas", 29, "Software Engineer");
person1.sayName(); //Nicholas
person1.name; //報錯(無法訪問)
person1.sayAge(); //報錯(無法訪問)
可見,利用C++定義出了一個Person
類,其中的name
、age
、job
以及sayAge()
是私有成員,無法通過類似person1.name
的方式直接訪問,這是一種類的保護機制;而定義為public
的sayName()
函數則可以直接訪問。
JS中的穩妥構造函數模式正是為了實現這樣的數據保護機制。它巧妙地利用了函數的作用域實現了對象屬性的私有化:在函數中定義的變量是局部變量,按道理本應該在函數執行完畢退出后進行銷毀或清理,但由于通過對象的公共方法對該局部變量保持著引用,所以該變量即便是在構造函數退出之后也依然保持有效(閉包)。
這樣一來,創建出的對象既能通過公共方法提供的訪問接口對私有屬性進行訪問(引用的是構造函數的局部變量),也能保證無法通過對象自身對其直接訪問(person1.name
無法訪問到對應數據,因為name
是構造函數的局部變量而不是對象的屬性),從而保證了對象屬性的訪問安全。