虽然Object构造函数或者对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象会产生大量的重复代码。为了解决这个问题,人们开始使用工厂模式的一种变体。
1、工厂模式
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(‘zcy‘,26,‘Software Engineer‘); var person2 = createPerson(‘bb‘,27,‘Software Engineer‘);
函数createPerson()能够根据接受的参数来构建一个包含所有必要信息的person对象。可以无数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型),随着javascript的发展,又一个新模式出现了。
2、构造函数模式
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = function(){ alert(this.name); }; } var person1 = new Person(‘zcy‘,26,‘Software Engineer‘); var person2 = new Person(‘bb‘,27,‘Software Engineer‘);
构造函数模式与工厂模式有以下不同之处:
没有显式地创建对象;
直接将属性和方法赋给了this对象;
没有return语句。
要创建Person的新实例,必须使用new操作符。这种方式调用构造函数实际上会经历以下4个步骤:
(1)创建一个新对象;
(2)将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
(3)执行构造函数中的代码(为这个新对象添加属性);
(4)返回新对象。
前面的例子中,person1和person2分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person。
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
检测对象类型,可以使用instanceof操作符,这个例子中国年创建的所有对象即是Object的实例,同时也是Person的实例。
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而正是构造函数模式胜过工厂模式的地方。
构造函数与其它函数的唯一区别,就在于调用它们的方式不同。不过构造函数毕竟也是函数,不存在定义构造函数的特殊语法,任何函数只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数也不会有什么两样。前面定义的Person()函数可以通过下列任何一种方式来调用。
//当作构造函数使用 var person = new Person(‘zcy‘,26,‘Software Engineer‘); person.sayName();//"zcy" //当作普通函数调用//当在全局作用域中调用一个函数时,this对象总是指向Global对象(在浏览器中是window对象) Person(‘zcy‘,26,‘Software Engineer‘); window.sayName();//"zcy" //在另一个对象的作用域中调用 var o= new Object(); Person.call(o,‘zcy‘,26,‘Software Engineer‘); o.sayName();//"zcy"
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍,在前面的例子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个function的实例。不要忘了ECMAScript中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。
alert(person1.sayName == person2.sayName); //false
创建两个完成同样任务的function实例的确没有必要,况且有this对象在,根本不用在执行代码前久把函数绑定到特定对象上面。因此可以如下把函数定义转移到构造函数外面来解决这个问题。
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(‘zcy‘,26,‘Software Engineer‘); var person2 = new Person(‘bb‘,27,‘Software Engineer‘);
这样做确实解决了两个函数做同一件事的问题,可是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在这些问题可以通过使用原型模式来解决。
3、原型模式
我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。如下所示。(个人理解,原型中的属性和方法相当于java类中的static属性和方法)
function Person(){ } Person.prototype.name = ‘zcy‘; Person.prototype.age = 26; Person.prototype.job = ‘Software Engineer‘; Person.prototype.sayName = function(){ alert(this.name); } var person1 = new Person(); person1.sayName(); //‘zcy‘ var person2 = new Person(); person2.sayName(); //‘zcy‘ alert(person1.sayName == person2.sayName);//true
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有的原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。如前面的例子,Person.prototype.constructor指向Person。而通过这个构造函数,我们还可以继续为原型对象添加其他属性和方法。
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object 继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版中管这个指针叫[[Prototype]]。要明确的一点是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性,如果在原型对象中找到了这个属性,则返回该属性的值。
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们再实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。实例中添加与原型同名的属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过使用delete操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性。
使用hasOwnProperty()方法可以检测一个属性是否存在于实例中还是存在于原型中,这个方法只在给定属性存在于对象实例中时,才会返回true。
另外,有两种方式使用in操作符,单独使用和在for-in循环中使用。在单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性是存在于实例还是原型中。所以同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。
在使用for-in循环时,返回的是所有能够通过对象访问的,可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记为false的属性)的实例属性也会在for-in循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的-----只有在IE8及更早版本中例外。
IE早期版本的实现中存在一个bug,即屏蔽不可枚举属性的实例属性不会出现在for-in循环中。
前面的原型例子更简单的写法是这种
function Person(){ } Person.prototype = { name:‘zcy‘, age:26, job:‘Software Engineer‘, sayName:function(){ alert(this.name); } };
此种写法与前面的不同之处是:constructor属性不再指向Person了。因为每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。而我们在这里使用的语法,本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。如果constructor的值真的很重要,可以像下面这样特意将它设置回适当的值。
function Person(){ } Person.prototype = { constructor:Person, name:‘zcy‘, age:26, job:‘Software Engineer‘, sayName:function(){ alert(this.name); } };
注意,以这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true。默认情况下原生的constructor属性是不可枚举的。
实例与原型之间存在松散连接关系,对原型对象的任何修改都能够立即从实例上反映出来,即使是先创建了实例后修改原型也是如此。因为实例与原型之间的连接是一个指针,而非一个副本。
但是如果是重新写整个原型对象,那么情况就会不一样。我们知道调用一个构造函数是会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系,请记住:实例中的指针仅指向原型,而不指向构造函数。
function Person(){ } var friend = new Person(); Person.prototype = { name:‘zcy‘, age:26, job:‘Software Engineer‘, sayName:function(){ alert(this.name); } }; friend.sayName(); //error
在这个例子中,我们先创建了Person的一个实例,然后又重写了其原型对象,然后在调用friend.sayName()时发生了错误,因为friend指向的原型中不包含以该名字命名的属性。
重写原型对象之前
重写原型对象之后
可以看出重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。
原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式的最大问题是由其共享的本性所导致的。
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适,对于那些包含基本值的属性倒也说的过去,毕竟通过在实例上添加同名属性可以隐藏原型中的对应属性。然而对于包含引用类型的属性来说问题就比较突出了。如下例子。
function Person(){ } Person.prototype = { constructor:Person, name:‘zcy‘, age:26, job:‘Software Engineer‘, friends:{‘Shelby‘,‘Court‘}, sayName:function(){ alert(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push(‘Van‘); alert(person1.friends); //"Shelby,Court,Van" alert(person2.friends); //"Shelby,Court,Van" alert(person1.friends == person2.friends); //true
可见,对原型中的引用类型修改会反映到所有实例上来。假如我们的初衷就是像这样,在所有实例中共享一个数组,那么这个结果没问题。可是,实例一般都是要有属于自己的全部属性的,而这个问题正是我们很少看到有人单独使用原型模式的原因所在。(此处修改原型中的引用类型与前面例子中在实例中给与原型中同名的基本类型属性赋值不同,在实例中给基本类型属性赋值只会屏蔽原型中的同名属性,并不会修改原型中同名属性的值)。
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(‘zcy‘,26,‘Software Engineer‘); var person2 = new Person(‘bb‘,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
这种混成模式,是目前在ECMAScript中使用最广泛的,认可度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。
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); }; } } var friend= new Person(‘zcy‘,26,‘Software Engineer‘); friend.sayName();
构造函数代码中加粗的部分,只有在sayName()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。且这里对原型所做的修改能够立即在所有实例中得到反映。其中if语句检查的可以使初始化之后应该存在的任何属性或方法,不必用一大堆if语句检查每个属性和每个方法,只检查其中一个即可。
使用动态原型模式时,不能使用对象字面量重写原型,因为如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。
6、寄生构造函数模式
通常在前述集中模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象,从表面上看,这个函数很像是典型的构造函数。如下例子。
function Person(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 friend = new Person(‘zcy‘,26,‘Software Engineer‘); friend.sayName(); //"zcy"
除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值得情况下,默认会返回新对象实例。而通过在构造函数末尾添加一个return语句,可以重写调用构造函数时返回的值。
这个模式可以在特殊情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式。
function SpecialArray(){ //创建数组 var values = new Array(); //添加值 values.push.apply(values,arguments); //添加方法 values.toPipedString = function(){ return this.join("|"); }; //返回数组 return values; } var colors = new SpecialArray("red","blue","green"); alert(colors.toPipedString()); //"red|blue|green"
关于寄生构造函数模式,有一点需要说明:首先返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖instanceof操作符来确定对象类型。
7、稳妥构造函数模式
稳妥对象(durable objects),指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this和new),或者在防止数据被其他应用程序(如Mashup程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不适用new操作符调用构造函数。如下例子。
function Person(name,age,job){ //创建要返回的对象 var o = new Object(); //可以在这里定义私有变量和函数 //添加方法 o.sayName = function(){ alert(name); }; //返回对象 return o; } var friend = Person("zcybb",26,"Software Engineer"); friend.sayName(); //"zcybb"
这种模式创建的对象中,变量friend中保存的是一个稳妥对象,而除了使用sayName()方法之外,没有其他办法访问name的值。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。
与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此instanceof操作符对这种对象也没有意义。