在学习《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
是构造函数的局部变量而不是对象的属性),从而保证了对象属性的访问安全。