虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。所以开始使用工厂模式的一个变体来解决这个问题。
一、工厂模式
该模式抽象了创建具体对象的过程。考虑到ECMAScript中无法创建类,开发人员就发明了一种函数,勇悍数来封装以特定接口创建对象的细节。如下所示:
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("huangQian",21,"science student");
var person2 = createPerson("MrHang",24,"graduate student");
person1.sayName();//huangQian
解析:函数createPerson()能够根据接受的参数来创建一个所有必要信息的person对象。可以无数次调用这个函数,它每次也都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了多个相似对象的问题,但没有解决对象识别的问题(即怎样知道一个对象的类型)。于是又有了下面的新模式
二、构造函数模式
1、ECMAScript中的构造函数可以用来创建特定类型的对象,像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。例如可以像下面一样利用构造函数将上面的例子重新写下:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
var person1 = new Person("huangQian",21,"science student");
var person2 = new Person("MrHang",24,"graduate student");
console.log(person1.age);//21
person2.sayName();//"MrHang"
解析:Person()函数取代了createPerson()函数,可以注意到Person()中的代码出除了与createPerson()中相同的部分外,还存在以下不同之处:
- 没有显式的创建对象
- 直接将属性和方法赋给了this对象
- 没有return语句
还可以注意到的是函数名Person使用的是大写字母P。按照惯例:构造函数始终都应该一个大写字母开头,而非构造函数应该以小写字母开头,目的是为了区别ECMAScript中的其它函数;因为构造函数本身也是函数,只不过是用来创建对象而已
2、要创建Person新实例,必须使用new操作符,这种方式调用构造函数实际上会经历以下四个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
3、在前面例子中所创建的对象person1、person2既是Object的实例,也是构造函数Person的实例,可以通过下面鉴定:
console.log(person1 instanceof Object);//true
console.log(person2 instanceof Object);//true
console.log(person1 instanceof Person);//true
console.log(person2 instanceof Person);//true
3、创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,这也正是构造函数模式胜过工厂模式的地方。
4、构造函数也存在着一些问题:前面的person1和person2对象都有一个名为sayName()的方法,但这两个方法不是同一个Function的实例。因为每定义一个函数就实例化了一个对象。前面例子中的每个Person实例都包含着一个不同的Function实例的本质。结果就是以这种个方式创建函数,会导致不同的作用域链和标识符解析。因此不同实例上的同名函数是不相等的。以下代码可以证明:
console.log(person1.sayName==person2.sayName);//false
因为创建两个完成同样任务的Function实例完全没有必要,不用在执行代码前就把函数绑定在特定对象上面。所以可以通过把函数定义转移到构造函数外部来解决这个问题,所以新的构造函数模式的优化代码可以如下:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
console.log(this.name);
}
经过上面的代码把sayName属性设置成全局的sayName()函数,那么person1和person2对象就共享了在全局作用域中定义的同一个sayName()函数。然而这样的话构造函数模式的问题也来了:**在全局作用域中定义的函数实际上只能被某个对象调用,让人会觉得全局作用域有点名不副实。而且如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。**然而这些问题可以通过下面的原型模式来解决
三、原型模式
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,如下面所示:
function Person(){
}
Person.prototype.name = "huangQian";
Person.prototype.age = 21;
Person.prototype.job = "science student";
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
person1.sayName();//"huangQian"
var person2 = new Person();
person2.sayName();//"huangQian"
console.log(person1.sayName==person2.sayName);//true
解析:上面代码将sayName()方法和属性直接添加到了Person的prototype属性中,构造函数变成了空函数。即便如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1和person2访问的都是同一组属性和同一个sayName()函数。