Object构造函数和对象字面量都可以创建单个对象,但是缺点明显:同一个接口创建多个对象,会产生大量重复代码。为了解决这个问题,人们开始使用工厂模式的一种变体。
一、工厂模式
工厂模式抽象了创建具体对象的过程。ECMA中无法创建类,开发人员发明一种函数,用以封装特定接口创建对象的细节,例如。
function createPerson(name,age){
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function(){
alert(this.name);
};
return o;
}
var p1 = createPerson("zhangsan",15);
var p2 = createPerson("lisi",20);
函数createPerson能够根据接收的参数来构建一个包含所有必要信息的Person对象。可以无数次地调用这个函数,而每次他都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(怎样知道一个对象的类型)。
二、构造函数模式
ES中的构造函数可以创建特定类型的对象。例如
function Person(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
alert(this.age);
}
}
var p1 = new Person("zhangsan",15);
var p2 = new Person("lisi",20);
此例子中Person()函数取代了createPerson()函数。我们注意到,Person()中的代码除了与createPerson()中相同的部分外,还存在一下不同之处:
- 没有显式地创建对象;
- 直接将属性和方法赋给了this对象;
- 没有return语句;
要创建Person实例,必须使用new操作符。以这种方式调用构造函数会经历一下4个步骤:
- 创建一个新对象;
- 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。
在前面例子的最后,p1和p2分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造器)属性,该属性指向Person,如下所示:
alert(p1.constructor == Person); //true
alert(p2.constructor == Person); //true
对象的constructor属性最初是用来标识对象类型的。但是,提到检测对象类型,instanceof更靠谱。我们此例子中创建的对象既是Person的实例,同时也是Object的实例,这点可以通过instanceof 验证。
构造函数模式之所以胜过工厂模式,是因为自定义的构造函数可以将它的实例标识为一种特定的类型。而所有对象均继承自Object。
1、将构造函数当成函数
构造函数与其他函数的唯一区别是——调用方式不同。任何函数,只要通过new操作符调用,那它就可以作为构造函数。反之,就和普通函数没有区别。前面定义的Person()函数可以通过下列任何一种方式来调用
// 当作构造函数使用
var person = new Person("zhangsan",15);
person.sayName(); // zhangsan
// 当作普通函数使用
Person("lisi",20);
window.sayName(); // lisi
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o,"wangwu",25);
o.sayName(); // wangwu
此例前两行代码展示了构造函数的典型用法,即使用new操作符创建一个新的对象。中间两行展示了不用new操作符调用Person()会出现什么结果:属性和方法都被添加到了window对象了。当在全局作用域中调用一个函数时,this对象总是指向Global对象(在浏览器中就是window对象)。因此调用完之后,可以通过window对象调用sayName()方法。最后,也可以使用call()或者apply在某个特殊对象的作用域中调用Person()函数。这里是在对象o的作用域中调用的,因此调用后o就拥有了所有属性和sayName()方法
2、构造函数存在的问题
构造函数虽好,但也有缺点。一个主要问题是:每个方法都要在每个实例上重新创建一遍。
alert(p1.sayName == p2.sayName); // false
然而,创建两个完成同样任务的Function实例完全没有必要。况且有this对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此可以把函数定义转移到构造函数外部来解决这个问题
function Person(name,age){
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
var p1 = new Person("zhangsan",15);
var p2 = new Person("lisi",20);
这样一来,由于p1、p2的sayName属性包含相同的指向全局函数sayName()的指针,因此就共享了全局作用域中的同一个函数。这样确实解决了两个函数做同一件事的问题,可是新的问题又来了:
在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在这些问题可以使用原型模式来解决。
三、原型模式
我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途就是 包含属性和方法,这些属性和方法可以由特定类型(该构造函数)的所有实例所共享。
function Person(){}
Person.prototype.name = "zhangsan";
Person.prototype.sayName = function(){
alert(this.name);
}
var p1 = new Person();
p1.sayName() // zhangsan;
var p2 = new Person();
p2.sayName() // zhangsan;
alert(p1.sayName == p2.sayName); // true;
1、理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。默认情况,所有的原型对象都会自动获得一个constructor属性,这个属性包含一个指针,指向prototype属性所在的函数。以下展示了各个对象之间的关系。
上图展示了Person构造函数、Person的原型属性和Person的两个实例之间的关系。Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。
原型对象的方法isPrototypeof()可以检测它是否是某个实例的原型对象。
alert(Person.prototype.isPrototypeOf(p1); //true
alert(Person.prototype.isPrototypeOf(p2); //true
 E5中增加了一个新方法,叫Object.getPrototypeOf(),该方法返回[[Prototype]]的值,例如:
alert(Object.getPrototypeOf(p1) == Person.prototype) //true
支持该方法的浏览器IE9+和其他
多个实例共享原型的属性和方法的原理——属性的搜索机制
每当代码读取某个对象的属性时,都会执行一次搜索。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找目标属性。
理解上述机制,原型的属性可以被屏蔽,但是不能被修改就很好理解了
function Person(){}
Person.prototype.name = "zhangsan";
Person.prototype.sayName = function(){
alert(this.name);
};
var p1 = new Person();
var p2 = new Person();
p1.name = "lisi";
alert(p1.name); // lisi
alert(p2.name); // zhangsan
delete p1.name;
alert(p1.name); // zhangsan
用原型的搜索机制很好解释上例的结果
方法hasOwnProperty()可以检测一个属性是否存在于实例中
alert(p1.hasOwnProperty("name") // 如果name是实例的属性,则true;如果是原型的属性,则false
2、原型与in操作符
in操作符有两种使用方式:1单独使用;2 for-in循环中使用
单独用时,in操作符会在能够访问到目标属性时返回true,不论属性是实例的还是原型的。例如
function Person(){}
Person.prototype.name = "zhangsan";
Person.prototype.sayName = function(){
alert(this.name);
};
var p1 = new Person();
var p2 = new Person();
alert(p1.hasOwnProperty("name")); //false
alert("name" in p1); // true
通过in操作符和hasOwnProperty()方法的组合,可以确定一个属性是否是原型属性
function isPrototypeProperty(object,name){
return !object.hasOwnProperty(name) && (name in object);
}
for-in循环时,返回所有能够通过对象访问的、可枚举的(enumerated)的属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性的实例属性也会在for-in循环中返回,因为根据定义,所有开发人员定义的属性都是可枚举的,Ie8及以前例外,ie早期版本存在一个bug,即屏蔽不可枚举属性的实例属性不会出现在for-in循环中。
要取得对象上所有可枚举的实例属性,可以用E5的Object.key()方法。如果要得到所有属性,无论可否枚举,可以使用Object.getOwnPropertyNames()。这两个方法都可以代替for-in循环。支持浏览器IE9+和其他
3、更简单的原型语法
为了避免每次添加一个属性,都要敲一遍Person.prototype,更常见的做法如下:
Person.prototype = {
name: "zhangsan",
age: 15,
sayName: function(){
alert(this.name);
}
};
上例将一个字面量对象设置为Person.prototype,结果基本相同,但是有一个例外:constructor属性不再指向Person了。因为本质上完全重写了默认的prototype对象,所以constructor属性也变成了新对象的constructor属性(指向Object构造函数)。此时instanceof操作符还能返回正确的结果,但constructor已经不行了
var p1 = new Person();
alert(p1 instanceof Person) //true
alert(p1.constructor== Person) // false
如果constructor真的很重要,需要保留,则可以:
Person.prototype = {
// 保留constructor的引用
constructor: Person,
name: "zhangsan",
age: 15,
sayName: function(){
alert(this.name);
}
};
注意这种方式会导致属性constructor的[[enumerable]]特性被设置为true,而原生的constructor属性是不可枚举的,可以用Object.defineProperty()测试。
4、原型的动态性
我们对原型的任何修改都能立即从实例中反应出来——即使先创建实例,后修改原型也如此。因为实例与原型之间的连接只是个指针而已,知道什么是引用类型就很容易理解了。
但是,如果重写整个原型对象,情况就不一样了。注意下列两种区别
function Person(){}
var p1 = new Person();
// 改变原型的属性
Person.prototype.name = "zhangsan";
Person.prototype.sayName = function(){
alert(this.name);
};
p1.sayName() // zhangsan
function Person(){}
var p1 = new Person();
// 改变了整个原型对象
Person.prototype = {
// 保留constructor的引用
constructor: Person,
name: "zhangsan",
age: 15,
sayName: function(){
alert(this.name);
}
};
p1.sayName() // 发生错误
示例二的过程如图
如图所示,重写原型,切断了与之前既存的的实例之间的联系;它们的引用仍然是最初的原型对象。
5、原生对象的原型
 js中的原生对象也在其原型上定义了一些公共方法。
6、原型对象存在的问题
原型上的属性和方法被所有实例共享,这种共享对函数非常合适。但是实例需要自己的空间,所以单独使用原型并不常见。
四、组合使用构造模式和原型模式
构造模式用户定义实例属性,原型用于定义方法和共享属性。这种混合模式是ES中使用最广泛、认同度最高的一种创建自定义类型的方法
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype = {
constructor:Person,
sayName:function(){
alert(this.name);
}
}
五、动态原型模式
有过其他OO语言经验的开发人员看到独立的构造函数和原型,会非常的困惑。动态原型模式就是要解决这个问题。
动态原型模式把所有信息都封装在了构造函数中,通过在构造函数中初始化原型(仅在必要的情况下),保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。例如
function Person(name,age){
this.name = name;
this.age = age;
// 方法绑定
if(typeof this.sayName != 'function'){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
// 测试
var p1 = new Person("zhangsan",15);
var p2 = new Person("zhangsan",15);
p1.sayName();
p2.sayName();
注意上例的方法绑定部分,该方法只会在初次调用构造函数时才会执行。此后,原型就已经给完成初始化,不会再执行。其中,if 语句检查的可以是初始化后应该存在的任何属性和方法,而且检查其中一个即可,不必全部检查。
使用动态原型模式时,不能使用对象字面量重写原型。因为在已经创建了实例的前提下重写原型,会切断现有实例和新原型之间的联系
扩展阅读
寄生构造函数模式
稳妥构造函数模式