虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方法在使用同一个接口创建很多对象时,会产生大量的重复代码。为了解决这个问题,人们开始使用工厂模式的一种变体。
一、工厂模式
工厂模式抽象了具体创建对象的过程,用函数来封装以特定接口创建对象的细节。如下例:
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");
工厂模式解决了用同一个接口创建对象产生重复代码的问题,但这种模式无法解决对象识别的问题。
二、构造函数模式
创建自定义的构造函数,意味着可以将它的实例表示为一种特定的类型。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
var person1 = createPerson("Nicholas",29,"Software Engineer");
var person2 = createPerson("Greg",27,"Doctor");
构造函数模式与工厂模式的区别:
- 没有显示的定义对象
- 直接将属性和方法赋给了this对象
- 没有return语句
要创建构造函数的实例,必须使用new操作符,以这种方式调用构造函数会经历以下4个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(this指向这个新对象)
- 执行构造函数中的代码
- 返回新对象
用构造函数创建的实例既是Object的实例,也是构造函数的实例。在上述例子中,person1既是Object的实例,也是Person的实例。
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
使用构造函数的主要问题,就是每个方法都在在每个实例上重新创建一遍。
alert(person1.sayName == person2.sayName); //false
三、原型模式
将某个特定类型的所有实例共享的属性和方法直接添加到原型对象中。
当创建一个新函数的时候,会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象Prototype。在默认情况下,所有的原型对象都会自动获得一个constructor属性,这个属性会包含一个指向prototype属性所在函数的指针。
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针[[Prototype]],指向构造函数的原型对象。
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();
var person2 = new Person();
上例中各个对象之间的关系如下:
每当代码读取某个对象的某个属性时,会先后执行两次搜索。首先,从实例对象本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值。否则,继续搜索指针指向的原型对象。如果在原型对象中找到了这个属性,返回该属性的值。
为了减少不必要的输入,可以用一个包含所有属性个方法的对象字面量重写整个原型对象。如下:
function Person(){
}
Person.prototype = {
name: 'Nicholas',
age: 29,
job: 'Software Engineer',
sayName: function(){
alert(this.name);
}
}
将函数的原型对象设置未一个以对象字面量形式创建的对象,结果是一样的。但是,在这种方法中,constructor属性不再指向Prototype属性所在函数(在创建函数时,就会同时创建函数的prototype对象,该对象会自动获取constructor属性。在这里使用了对象字面量重写了prototype对象,所以constructor属性也就变成了新对象的constructor属性,也就是Object构造函数)。想要constructor属性指向Prototype属性所在函数,可以在对象字面量中显示的定义。如下:
function Person(){
}
Person.prototype = {
constructor: Person,
name: 'Nicholas',
age: 29,
job: 'Software Engineer',
sayName: function(){
alert(this.name);
}
}
原型模式存在以下两个问题:
- 所有实例在默认情况下都将取得相同的属性值
- 对某个属性做修改时,对包含基本值的属性来说,会在实例上添加一个同名属性,可以隐藏原型中的对应属性。但对包含引用类型的属性做修改时,会影响所有的实例。
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.name = 'Bob';
console.log(person1.name); //Bob
console.log(person2.name); //Nicholas
console.log(person1.name === person2.name); //false
person1.friends.push('Van');
console.log(person1.friends); //'Shelby,Court,Van'
console.log(person2.friends); //'Shelby,Court,Van'
console.log(person1.friends === person2.friends); //true
四、组合使用构造函数模式与原型模式
这是最常见的方法。构造函数用于定义实例属性,而原型模式用于定义方法与共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用。
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 person1 = 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
五、动态原型模式
把所有的信息都封装在了构造函数中,而通过在构造函数中初始化原型。也就是在构造函数中,通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['Shelby','Court'];
if(typeof this.sayName != 'function'){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
使用动态原型模式,不能使用对象字面量重写原型。而且添加原型属性的代码,只会在初次调用构造函数时才会执行。