1、工厂模式
由于ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节。
1 function createPerson( name, age, job ){ 2 var o = new Object(); 3 o.name = name; 4 o.age = age; 5 o.job = job; 6 7 o.sayName = function(){ 8 alert( this.name ); 9 }; 10 11 return o; 12 }
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别问题(即怎样知道一个对象的类型)。
2、构造函数模式
ECMAScript 中的构造函数可以用来创建特定类型的对象。
1 function Person( name, age, job ){ 2 this.name = name; 3 this.age = age; 4 this.job = job; 5 6 this.sayName = function(){ 7 alert( this.name ); 8 }; 9 } 10 11 var person1 = new Person( "Nicholas", 29, "Software Engineer" ); 12 var person2 = new Person( "Greg", 27, "Doctor" );
在上面这个例子中,person1和person2分别保存中Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person。
1 alert( person1.constructor == Person ); //true 2 alert( person2.constructor == Person ); //true
1)将构造函数当做函数
构造函数与其他函数的唯一区别,就在于调用它们的方法不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那它就可以作为构造函数。而任何函数,如果不是通过new操作符来调用,那它 跟普普通函数也不会有什么两样。
1 //当做构造函数使用 2 var person = new Person( "Nicholas", 29, "Software Engineer" ); 3 person.sayName(); //"Nicholas" 4 5 //作为普通函数调用 6 Person( "Greg", 27, "Doctor" ); 7 window.sayName(); //"Greg" 8 9 //在另一个对象的作用域中调用 10 var o = new Object(); 11 Person.call( o, "Kristen", 25, "Nurse" ); 12 o.sayName(); //"Kristen"
2)构造函数的问题
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1和person2都有一个额名为sayName()的方法,但是那两个方法并不是同一个Function的实例。
在ECMAScript中函数是对象,因此每定义一个函数,也就是实例化了一个对象。
而创建两个完成同样任务的Function实例的确没有必要。况且有this对象在,根本不用在执行代码前就把函数绑定到特定的对象上,可以通过这样的形式定义:
1 function Person( name, age, job ){ 2 this.name = name; 3 this.age = age; 4 this.job = job; 5 6 this.sayName = sayName; 7 } 8 9 function sayName(){ 10 alert( this.name ); 11 }
如此一来,就可以将sayName()函数的定义转移到构造函数外部。而在构造函数内部,我们将sayName属性设置成全局的sayName函数。这样的话,由于sayName包含的是一个指向函数的指针,因此person1和person2对象就可以共享在全局作用域中定义的同一个sayName()函数。
这样做解决了两个函数做同一件事的问题,但是新的问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更重要的是:如果对象需要定义很多方法,那么就需要定义很多个全局函数,这样一来,我们自定义的这个引用类型就毫无封装性可言了。
这些问题可以通过使用原型模式来解决。
3、原型模式
我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个对象,它的用途是包含可以由特定类型的所有实例共享的属性和方法。
使用原型的好处是可以让所有对象实例共享它所包含的属性和方法。
1 function Person(){ 2 } 3 4 Person.prototype.name = "Nicholas"; 5 Person.prototype.age = 29; 6 Person.prototype.job = "Software Engineer"; 7 Person.prototype.sayName = function(){ 8 alert( this.name ); 9 }; 10 11 var person1 = new Person(); 12 person1.sayName(); //"Nicholas" 13 14 var person2 = new Person(); 15 person2.sayName(); //"Nicholas" 16 17 alert( person1.sayName == person2.sayName ); //true
1)理解原型
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性。
在默认情况下,所有prototype属性都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。
每当代码读取某个对象的某个属性时,都会执行一搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。
如果我们在实例中添加了一个属性,而该属性与实例中的一个属性同名,那么就会在实例中创建该属性,该属性将会屏蔽原型中的那个属性。
即使是将属性设置为null,也只是在实例中的属性值为null。
不过,使用delete操作符可以完全删除实例属性,从而能够重新访问原型中的属性。
使用hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在与原型中。这个方法只在给定属性存在于对象实例中时,才会返回true。
2)原型与in操作符
in操作符会在通过对象能够访问给定属性时返回true,无论该属性是存在于实例中还是原型中。
3)更简单的原型语法
function Person(){ } Person.prototype = { name : "Nicholas", age : 29, job : "Software Engineer", syaName : function(){ alert( this.name ); } };
使用这个方式添加原型属性实际上相当于重新创建了一个原型对象。因此,constructor属性不再指向Person了。而是指向了Object构造函数。因此,尽管使用instanceof操作符还能够返回正确的结果,但是通过constructor已经无法确定对象的类型了。
var person = new Person(); alert( person instanceOf Object ); //true alert( person instanceOf Person ); //true alert( person.constructor == Person ); //false alert( person.constructor == Object ); //true
因此需要在prototype中重新为constructor指定值:constructor : Person
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 ); } };
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 ); }; } }
其中if区块中的语句只会在构造函数初次调用时执行。伺候原型已经完成初始化,不需要再做修改了。
if语句检查的可以使初始化之后应该存在的任何属性或方法,不需要使用一大堆的if语句检查每个属性和每个方法,只需要检查其中的一个即可。
注意:在使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。