1.对象字面量
var person = {
name: "John",
age: 18,
job: "student",
sayName: function(){
alert(this.name);
}
};
person.sayName(); //"John"
对象自面量虽然可以用来创建单个对象,但有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题人们开始使用工厂模式的一种变体。
2.工厂模式
工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。考虑到ECMAScript中无法创建类,用函数来封装以特定接口创建对象的细节。
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.name = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("John", 18, "student");
var person2 = createPerson("Grey", 27, "doctor");
函数createPerson()能够根据接受的参数来构建一个包含所有必要信息的Person对象。可以无数次的调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。
工厂模式虽然解决了创建多个相似对象的问题,但没有解决对象时别的问题(即,怎样知道一个对象的类型)。
3.构造函数模式
ECMAScript中的构造函数可用来创建特定类型的对象。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外,还可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。
function Person(){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
var person1 = new Person("John", 18, "student");
var person2 = new Person("Grey", 27, "doctor");
要创建的新实例,必须使用new操作符。以这种方式调用构造函数实际上会经历以下四个步骤:
(1)创建一个新对象
(2)将构造函数的作用域赋给新对象(因此this指向了这个新对象)
(3)执行构造函数中的代码(为这个新对象添加属性)
(4)返回新对象
在这个例子中,person1和person2分别保存着Person的一个不同的实例。这两个对象都有一个constructor属性,该属性指向Person。如下所示:
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
对象的constructor属性最初是用来标识对象类型的。但是检测对象类型还是使用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。
构造函数模式虽然好用,但也有缺点,使用构造函数的主要问题,就是每个方法都要在每个实例上重新定义一遍。上面的例子中,person1和person2都有一个sayName()的方法,但那两个方法不是同一个Function的实例,因此每定义一个函数,也就是实例化了一个对象。从逻辑上讲,此时的构造函数也可以这样定义:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
//与声明函数在逻辑上是等价的
this.sayName = new Function("alert(this.name)");
}
每个Person实例都包含一个不同的Function实例。以这种方式创建函数,会导致不同的作用域链和标示符解析,但创建Function新实例的机制任然是相同的。因此,不同实例上的同名函数是不相等的。
可以通过将函数定义转移到构造函数外部来解决这个问题,例如:
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("John", 18, "student");
var person2 = new Person("Grey", 27, "doctor");
如果对象需要定义很多方法,就要定义很多个全局变量,于是这个自定义类型的引用类型就没有封装性可言了,这些问题可以通过原型模式来解决。
3.原型模式
使用原型对象的好处是可以让所有实例对象共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,如下面的例子所示:
function Person(){
}
Person.prototype.name = "John";
Person.prototype.age = 18;
Person.prototype.job = "student";
Person.prototype.syaName = function(){
alert(this.name);
}
var person1 = new Person();
person1.syaName(); //"John"
var person2 = new Person();
person2.sayName(); //"John"
alert(person1.sayName == person2.sayName); //true
原型模式也有缺点。首先,它忽略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。原型中所有的属性是被很多实例共享的,这种共享对于函数非常合适,但对于引用函数类型值的属性来说,问题就比较突出了。
function Person(){
}
Person.prototype = {
constructor: Person,
name: "John",
age: 18,
job: "student",
friends: ["Shelby", "Court"],
sayName: function(){
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count,Van"
alert(person1.friends == person2.friends); //true
由于friends数组存在于Person.prototype而非Person1中,所以修改了friends之后会显示在person2中。
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("john", 18, "student");
var person2 = new Person("Grey", 27, "doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends == person2.friends); //false
alert(person1.sayName == person2.sayName); //true
在这个实例中,实例属性都是在构造函数中定义的,而由所有实例共享的属性constructor和方法sayName()则是在原型中定义的。
这种构造函数与原型混成的模型,是目前ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。
5.动态原型模式
动态原型模式把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型,又保持了同时使用构造函数和原型的优点。
function Person(name, age, job){
//属性
this.name = name;
//方法
if (typeof this.sayName != "function") {
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var friend = new Person("John", 18, "student");
friend.sayName();
使用动态原型模式时,不能使用对象自面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。
6.寄生构造函数模式
通常,在前述的几种模式都不适用的情况下,可以使用寄生构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。例如:
function Person(){
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("John", 18, "student");
friend.sayName(); //"John"
关于寄生构造函数的几点说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系,也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖instanceof操作符来确定对象类型。由于存在上述问题,建议在可以使用其他模式的情况下,不要使用这种模式。
7.稳妥构造函数模式
稳妥对象最适合在一些安全的环境中,或者在防止数据被其它应用程序改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this,二是不使用new操作符调用构造函数。
function Person(name, age, job){
//创建要返回的对象
var o = new Object();
//k=可以在这里添加定义私有变量和函数
//添加方法
o.sayName = function(){
alert(name);
}
//返回对象
return o;
}
在上述的例子中,除了调用sayName()方法外,没有别的方式可以访问其数据成员。稳妥构造函数提供的这种安全性,使得它非常适合在某些安全执行环境下使用。
参考资料:《JavaScript高级程序设计》(第三版)
(完)