以下是基于《JavaScript高级程序设计》一书,对创建对象的几种方法所进行的整理。虽然工作中经常用到,但是概念性的东西,也是要整明白的呀,不仅会用,还要明白这是什么,为什么这么用 才是王道。
1、工厂模式
用函数来封装以特定接口来创建对象。
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"
函数createPerson()
能够根据接受的参数来创建一个包含所有必要信息的Person
对象。可以无数次的调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。
- 缺点:虽然解决来创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
2、构造函数模式
构造函数可以用来创建特定类型的对象,使用构造函数重写上个例子:
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");
可见,Person()
函数与createPerson()
不同之处在于:
- 没有显示的创建对象;
- 直接将属性和方法赋给来
this
对象; - 没有
return
语句; - 使用
new
操作符来创建实例。
扩展 - 使用new
操作符来创建实例实际上会经历以下4个步骤:
1、创建一个新对象;
2、将构造函数的作用域赋给新对象(因此this
就指向了这个新对象);
3、执行构造函数中的代码;
4、返回新对象。
在这个例子中所创建的实例person1
和person2
即是Person
的实例,同时也是Object
的实例,通过instanceof
操作符可以进行验证。
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定类型,而这正是构造函数模式胜过工厂模式的地方。
- 缺点:使用构造函数模式的主要问题,就是每个方法都要在每个实例上重新创建一遍。在上面例子中,
person1
和person2
都有一个名为sayName()
的方法,但是那两个方法不是同一个Function
实例,会导致不同的作用域链和标识符解析,不同实例上的同名函数是不相等的。
alert(person1.sayName == person2.sayName); //false
创建两个完成同样任务的Function
实例是没有必要的,况且有this
对象在,根本不用在执行代码前就把函数绑定在特定对象上面。因此,会有人提出把函数定义在构造函数外部来解决这个问题。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
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); //true
把sayName()
函数定义到构造函数外部之后,从上面代码可见,除了sayName()
函数外,其他的实现与前面代码完全一致。
而在构造函数内部,将sayName
属性设置成等于全局的sayName()
函数。由于sayName
是一个指向函数的指针,因此person1
和person2
对象就共享来在全局作用域中定义的一个sayName()
函数,这样做确实解决来两个函数做同一件事。可是又产生来新的问题:
- 在全局作用域定义的函数只能被某个对象调用,这样让全局作用域有点名不副实;
- 若是对象需要定义很多方法,那么需要定义多个全局函数,于是这个自定义的引用类型就毫无封装性可言了。
3、原型模式
我们创建的每个函数都有一个prototype
(原型)属性,这个属性指向通过调用构造函数而创建的实例的原型对象,对原型还不熟悉的小伙伴可先看看这篇this、原型和作用域。
使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
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();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
在此,我们将sayName()
方法和所有属性直接添加到了Person
的prototype
属性中,构造函数变成了空函数,即是如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的,person1
和person2
访问的都是同一组属性和同一个sayName()
函数。
- 缺点:
1、它省略来为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值;
2、由原型共享的本性所导致的,修改一个实例的属性,会对另一个实例的属性也造成影响,见以下代码:
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
4、组合使用构造函数+原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式+原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,这样,每个实例都会有自己的一份实例属性的副本,并且同时又共享着怼方法的引用,最大限度的节省了内存。另外,这种混合模式还支持向构造函数传递参数,集两种模式之长。下面代码重写了前面的例子:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor: Person,
sayName : function () {
alert(this.name);
}
};
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
在上面例子中,实例属性都是在构造函数中定义的,而所有实例共享的属性constructor
和方法sayName()
则是在原型中定义的。而修改了person1.friends
(向其中添加一个新字符串),并不会影响到person2.friends
,因为他们分别引用了不同的数组。
可以说,这是目前用来定义引用类型的一种默认模式。
5、动态原型模式
有其他OO语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到困惑。动态原型模式正是致力于解决这个问题的一个方案,它把所有信息都封装在了构造函数中,而通过构造函数中初始化原型(仅在必要条件下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
function Person(name, age, job){
// 属性
this.name = name;
this.age = age;
this.job = job;
// 方法
if (typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
注意方法部分,这里只在sayName()
方法不存在的情况下,才会将他添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了,这里对原型所做的修改,能够立即在所有实例中得到反映。
注意,使用动态原型模式时,不能使用对象字面量重写原型,在this、原型和作用域中提到的,如果在已经创建了实例的情况下重写原型,会切断现有实例与新原型之间的关系。
6、寄生构造函数模式
这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像经典的构造函数。
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 friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
在这个例子中,Person
函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。除了使用 new
操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return
语句,可以重写调用构造函数时返回的值。
这个模式可以在特殊情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改Array
构造函数,因此可以使用这个模式。
function SpecialArray(){
// 创建数组
var values = new Array();
// 添加值
values.push.apply(values, arguments);
//添加方法
values.toPipedString = function(){
return this.join("|");
};
// 返回数组
return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); // "red|blue|green"
alert(colors instanceof SpecialArray);
关于寄生构造函数模式,有一点需要说明:返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖instanceof
操作符来确定对象类型。由于存在上述问题,在可以使用其他模式的情况下,不要使用这种模式。
7、稳妥构造函数模式
首先,要明白什么是稳妥对象:指没有公共属性,而其他方法也不引用this
的对象。
稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this
和new
),或者防止数据被其他应用程序(如Mashup程序)改动时使用。
稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this
;二是不适用new
操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的Person
构造函数重写:
function Person(name, age, job){
// 创建要返回的对象
var o = new Object();
// 可以在这里定义私有变量和函数
// 添加方法
o.sayName = function(){
alert(this.name);
};
// 返回对象
return o;
}
var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
注意,在这种模式创建的对象中,除了使用sayName()
方法之外,没有其他方法访问name
的值。即使有其他代码会给这个对象添加方法和数据成员,但也不可能有别的方法访问传入到构造函数中的原始数据。
稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境下使用。
与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此 instanceof
操作符对这种对象也没有意义。
总结
- 工厂模式:
使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。这个模式后来被构造函数模式所取代。 - 构造函数模式:
可以创建自定义引用类型,可以像创建内置对象实例一样使用new
操作符。不过,构造函数模式也有缺点,即它的每个成员都无法得到复用,包括函数。由于函数可以不局限于任何对象(即与对象具有松散耦合的特点),因此在多个对象间共享函数。 - 原型模式:
使用构造函数prototype
属性来指定那些应该共享的属性和方法。组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,而使用原型定义共享的方法和属性。