众所周知,虽然对象字面量刻意用来创建单个对象,但是有一个明显的缺点,使用同一个接口创建很多对象,会产生大量重复的代码。为解决这一问题,人们开始使用工厂模式
1.工厂模式
function createPerson(name,age){
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function(){
console.log(this.name);
};
return o
}
var person1 = createPerson('ll',20)
var person2 = createPerson('ww',21)
person1.sayName(); //ll
函数createPerson()能够根据接受到的参数来构建一个包含所有必要信息的Person对象。可以无数次的调用这个函数,而每次它都会返回一个包含两个属性和一个方法的对象。
缺点:工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
二.构造函数模式
可以使用构造函数模式将前面的例子重写如下:
function Person(name,age,job){
this.name= name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
var person1 = new Person('ll',19,'student');
var person2 = new Person('ww',21,'web');
使用构造函数的主要问题:就是每个方法都要在每个实例上重新创建一遍,在上面的例子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Function的实例,创建两个完成同样任务的Function实例的确没有必要;况且有this对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此可以像下面这样,通过把函数定义转移到构造函数外部来解决这个问题。
注:不同实例上的同名函数是不相等的,
alert(person1.sayName == person2.sayName) //flase
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('ll',19,'student');
var person2 = new Person('ww',21,'web');
在这个例子中,我们把sayName()函数的定义转移到构造函数外部。而在构造函数内部,我们将sayName属性设置成等于全局的sayName函数,这样person1和person2对象就共享了在全局作用域中定义的同一个sayName()函数。这样确实解决了两个函数在做同一件事的问题,可是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多全局函数。好在,这些问题可以通过使用原型模式来解决。
三.原型模式
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有的对象实例共享他所包含的属性和方法。
function Person(){}
Person.prototype.name = 'll';
Person.prototype.age = '20';
Person.prototype.job = 'web';
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
虽然我们可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值,如果我们再实例中添加一个属性,而该属性实例原型中的一个属性名同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。不过可以使用delete删除属性
function Person(){}
Person.prototype.name = 'll';
Person.prototype.age = '20';
Person.prototype.job = 'web';
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = 'lh';
console.log(person1.name) //'lh' 来自实例
conosole.log(person2.name) //'ll' 来自原型
delete person1.name;
console.log(person1.name) //'ll' 来自原型
原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式的最大问题是由共享的本性所导致的。
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说的过去,通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然后,对于包含引用类型的属性来说,问题就比较突出了。
function Person(){}
Person.prototype = {
constructor:Person,
name:'ll',
age :'20',
job :'web',
friends:['ww','tt'],
sayName : function(){
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push('hh');
alert(person1.friends); //'ww,tt,hh'
alert(person2.friends); //'ww,tt,hh'
alert(person1.friends === person2.friends); //true
四.组合使用构造函数模式和原型模式
组合使用构造函数模式和原型模式,是创建自定义类型的最常见方式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度的节省了内存。
function Person(name,age,job){
this.name= name;
this.age = age;
this.job = job;
this.friends = ['ll','ww'];
}
Person.prototype = {
constructor:Person,
sayName : function(){
alert(this.name);
}
}
var person1 = new Person('tt',20,'java',);
var person2 = new Person('ss',25,'php');
person1.friends.push('aa');
alert(person1.friends); //'ll,ww,aa'
alert(person2.friends); //'ll,ww'
alert(person1.friends === person2.friends); //false
在上面的例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性constructor和方法satName()则是在原型中定义的。而修改了person1.friends(向其中添加一个新字符串),并不会影响到person2.friends,因为他们分别引用了不同的数组。
五.动态原型模式
动态原型模式它把所有的信息都封装在了构造函数中,而通过构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否要初始化原型。来看一个例子:
function Person(name, age){
this.name = name;
this.age = age;
this.friends = ["乾隆","康熙"];
//注意if语句
if(typeof this.sayName!="function"){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
var person1 = new Person("wei",29);
person1.friends.push("嬴政");
person1.sayName();
注意构造函数代码中的if语句,这里只在sayName()方法不存在的情况下才会将它添加到原型中。这断代码只有在第一次调用构造函数的时候才会被执行。此后,原型已经被初始化,不需要再做什么修改。不过要记住,这里所做的修改能立即在所有实例中得到反映。因此,这种方法可以说确实非常完美。其中if语句检查的是初始化之后应该存在的任何方法和属性–不必再用一大堆if来检查每个属性和方法,只检查其中一个即可。对于采用这样模式创建的对象,还可以使用instanceof操作符来确定他的类型。
注意:使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有的实例与新原型之间的联系。
六.寄生构造函数模式
通常,在上述几种模式都不适合的情况下可以使用寄生构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象,但从表面看,这个函数又很像典型的构造函数。来看一个例子:
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 person = new Person("wei",29,"banzhuan");
person.sayName(); //"wei"
在这个例子中,Person函数创建了一个对象,并以相应的属性和方法初始化该对象,然后返回了这个对象。除了使用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");
console.log(colors.toPipedString()); //red|blue|green
在这个例子中,我们创建了一个名为SpecialArray的构造函数。在这个函数的内部,首先创建了一个数组,然后push()方法初始化了数组的值。随后又给数组实例添加了toPipedString()方法,用来返回以竖线分隔的数组值。最后将数组以函数的形式返回。接着,我们调用了SpecialArray构造函数,传入了初始化的值,并调用了toPipedString()方法。
关于寄生构造函数模式,有一点需要声明:首先,返回的对象与构造函数或者构造函数的原型没有任何关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖instanceof操作符来确定对象的类型。由于存在这一的问题,我们建议在可以使用其他模式的情况下不要使用这种模式
七、稳妥构造函数模式
道格拉斯·克拉克福德发明了JavaScript中的稳妥对象这个概念。所谓稳妥对象,是指没有公共属性,而且其方法也不引用this对象。稳妥对象最适合在一些安全环境中(这些环境会禁止使用this和new),或者在防止数据被其他应用程序改动时使用。稳妥构造函数遵循的与寄生构造函数类似的模式,但又两点不同:一是新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的Person构造函数重写如下:
function Person(name, age, job){
//创建要返回的新对象
var o = new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName = function(){
alert(this.name);
};
//返回对象
return o;
}
注意,在以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问name的值。可以像下面使用稳妥的Person构造函数:
var person =Person("weiqi",22,"banzhuan");
person.sayName(); //weiqi
这样,变量person中保存的是一个稳妥对象,而除了sayName()方法外,没有别的方式可以访问其他数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。稳妥构造函数模式提供的这种安全性,使得他非常适合在某些安全执行环境–例如,ADsafe(ADsafe)提供的环境下使用。
注意:与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间没有什么关系,因此instanceof操作符对这种对象也没有意义。