虽然Object构造函数或对象字面量都可以用来创建单个对象,但这种方式有个弊端:使用同一个接口创建很多对象,会产生大量的重复代码。为了解决这个问题,于是百家争鸣,各种工厂模式的变体应运而生。
1.工厂模式
这种模式是软件工厂领域的广为人知的设计模式,它抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节,举个栗子:
1 function createPerson(name,age,job){
2 var o=new Object();
3 o.name=name;
4 o.age=age;
5 o.job=job;
6 o.sayname = function(){
7 alert(this.name);
8 };
9 return o;
10 }
11
12 var person1=createPerson("Sleipnir",23,"Software");
13 var person2=createPerson("xiaoxiao",24,"Student");
这个模式虽然可以无数次调用,解决了创建多个相似对象的问题,但没有解决对象识别的问题(即怎样知道一个对象的类型)
2.构造函数模式
我们先用构造函数模式把工厂模式的例子重写一遍:
1 function Person(name,age,job){
2 this.name=name;
3 this.age=age;
4 this.job=job;
5 this.sayname = function(){
6 alert(this.name);
7 };
8 }
9
10 var person1=new Person("Sleipnir",23,"Software");
11 var person2=new Person("xiaoxiao",24,"Student");
跟之前的工厂模式,我们可以看出区别:1.没有在函数内再创建对象
2.直接将属性和方法赋给了this对象
3.没有return语句
在创建Person的实例时,必须用到new操作符。以这种方式调用构造函数会经历以下4个步骤:1.创建一个新对象 2.将构造函数的作用域赋值给新对象(因此this就指向了这个新对象) 3.执行构造函数中的代码(为新对象添加属性和方法) 4.返回新对象
刚才说了,构造函数胜于工厂模式的大方在于,它能解决实例的对象类型问题,将来可以将它的实例标识为一种特定类型:
1 console.log(person1 instanceof Person); //true 2 console.log(person1 instanceof Object]); //true 3 console.log(person2 instanceof Person); //true 4 console.log(person2 instanceof Object); //true 5 console.log(Person instanceof Object); //true
person1和person2之所以同时是Object的实例,是因为所有对象均继承自Object(下面会讲到继承)
2.1 调用构造函数的方式
上面已经写了Person的构造函数,我们来用几种不同的方式调用:
1 //当做构造函数使用
2 var person= new Person("Sleipnir",23,"Software");
3 person.sayname(); //"Sleipnir"
4
5 //作为普通函数使用
6 Person("xiaoxiao",25,"Student"); //添加到全局window
7 window.sayname(); //"xiaoxiao"
8
9 //在另一个对象的作用域中调用
10 var o=new Object();
11 person.call(o,"xiaoxiao",25,"Student");
12 o.sayname(); //"xiaoxiao"
第一种是当做构造函数使用,之前已经提过,;第二种是作为普通函数调用,因为this对象都是指向全局对象,所以属性和方法都被添加了window对象;第三种是在o对象的作用域中使用call()方法来调用Perso函数
2.2 构造函数的问题
构造函数虽然不错,但也有瑕疵。主要问题就是每个方法需要在每个实例上重新创建一遍,但是,如果在构造函数里面创建Function实例或者在构造函数外部定义函数来供构造函数创建的实例调用的话,那我们所谓的构造函数的全局作用域就名不副实,自定义的引用类型包括定义的方法也就没有封装性可言了。
好在程序员们都是不服输的人,历史的车轮总是向前滚动,于是原型模式就登上历史舞台了。
3.原型模式
概念先不说,举个栗子再解释:
1 function Person(){
2 }
3 Person.prototype.name="Sleipnir";
4 Person.prototype.age=23;
5 Person.prototype.job="Software";
6 Person.prototype.sayname=function(){
7 alert(this.name);
8 };
9
10 var person1=new Person();
11 person1.sayname(); //"Sleipnir"
12 var person2=new Person();
13 person2.sayname(); //"Sleipnir"
14
15 alert(person1.sayname==person2.sayname);
我们创建的每一个函数都有一个prototype(原型)属性,这个属性是个指针,指向一个对象。所以,prototype就是通过调用构造函数而创建的那个对象实例的原型对象。上面例子中的person1和person2所拥有的属性和方法都是来源于构造函数的原型对象中的
想要理解原型模式,就要理解【构造函数】【原型】【实例】这三种之间的关系,抽象出来简单来说,原型是构造函数的属性,而实例是通过构造函数的原型而创建的,实例和构造函数没有关系,原型里还有一个constructor是指向构造函数的,constructor就是类似于指针的存在,构造函数通过constructor的指针作用,就把原型和实例连接起来了。这也是最简单的原型继承关系。
3.1 访问实例/原型的属性
有时候,我们根据原型创建的实例,这个实例里有一部分是原型的属性,有一部分也是实例自己的属性,于是就要判断哪些属性属于原型,哪些属性属于实例自己
有两个方法:hasOwnProperty() 和 in操作符
先看hasOwnProperty():
1 function Person(){
2 }
3 Person.prototype.name="Sleipnir";
4 Person.prototype.age=23;
5 Person.prototype.job="Software";
6 Person.prototype.sayname=function(){
7 alert(this.name);
8 };
9
10 var person1=new Person();
11 var person2=new Person();
12
13 console.log(person1.hasOwnProperty("name")); //false
14
15 person1.name="xiaoxiao";
16 console.log(person1.name); //"xiaoxiao"
17 console.log(person1.hasOwnProperty("name")); //true
18
19 console.log(person2.name); //"Sleipnir"
20 console.log(person2.hasOwnProperty("name")); //false
21
22 delete person1.name;
23 console.log(person1.name); //"Sleipnir"
24 console.log(person1.hasOwnProperty("name")); //false
从代码结果可以总结出来,hasOwnProperty()检测的是实例属性,如果是属于实例的,返回true,否则返回false
in操作符:
function Person(){
}
Person.prototype.name="Sleipnir";
Person.prototype.age=23;
Person.prototype.job="Software";
Person.prototype.sayname=function(){
alert(this.name);
};
var person1=new Person();
var person2=new Person();
console.log(person1.hasOwnProperty("name")); //false
console.log("name" in person1); //true
person1.name="xiaoxiao";
console.log(person1.name); //"xiaoxiao"
console.log(person1.hasOwnProperty("name")); //true
console.log("name" in person1); //true
console.log(person2.name); //"Sleipnir"
console.log(person2.hasOwnProperty("name")); //false
console.log("name" in person2); //true
delete person1.name;
console.log(person1.name); //"Sleipnir"
console.log(person1.hasOwnProperty("name")); //false
console.log("name" in person1); //true
通过互相比较,我们能看出来,直接使用in操作符是,无论属性是实例自己的还是原型的,都会返回true
我们来定义一个函数,组合使用hasOwnProperty()和in操作符:
function hasPrototypeProperty(object,name){
return !object.hasOwnProperty(name)&&(name in object);
}
1 function Person(){
2 }
3 Person.prototype.name="Sleipnir";
4 Person.prototype.age=23;
5 Person.prototype.job="Software";
6 Person.prototype.sayname=function(){
7 alert(this.name);
8 };
9 function hasPrototypeProperty(object,name){
10 return !object.hasOwnProperty(name)&&(name in object);
11 }
12 var person=new Person();
13 console.log(hasPrototypeProperty(person,"name")); //true
14
15 person.name="xiaoxiao";
16 console.log(hasPrototypeProperty(person,"name")); //false
3.2 更简单的原型语法
使用Person.prototype一个个定义属性太繁琐,于是我们可以用对象字面量给原型定义属性:
1 function Person(){
2 }
3 Person.prototype={
4 constructor:Person,
5 name:"Sleipnir",
6 age:23,
7 job:"Software",
8 sayname: function(){
9 alert(this.name);
10 }
11 };
这里要注意,constructor要设置为Person,不然就会切断原型与实例之间的关系了
3.3 原型模式的问题
原型模式省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都取得了相同的属性值。
原型模式的问题在于其共享的本质,我们来看一个由于这个原因而引起问题的栗子:
function Person(){
}
Person.prototype={
constructor:Person,
name:"Sleipnir",
age:23,
job:"Software",
friends:["zhangsan","lisi"],
sayname: function(){
alert(this.name);
}
};
var person1=new Person();
var person2=new Person();
person1.friends.push("van");
console.log(person1.friends); //"zhangsan,lisi,van"
console.log(person2.friends); //"zhangsan,lisi,van"
console.log(person1.friends===person2.friends); //true
这个例子中,friends是个引用类型的数组,由于在原型中已经定义了,所以在实例中做修改时,修改的值也会映射到其原型,然后原型又会同时将数据更新加载到其他实例中,这也体现了原型的动态性。但这种效果并不是我们想要的。
实际需求是,原型帮我们定义一部分公共属性和方法,然后实例自己也有独立的属性和方法,基于这种情况,便有了下面的模式。
4.组合使用构造函数模式和原型模式
在这种模式下,构造函数用于定义实例属性,原型模式用于定义方法和共享的属性。结果,每个实例都有了自己的一份实例属性的副本,同时也共享着对方法的引用,最大限度地节省内存,同时这种模式还支持向构造函数传递参数,可谓集两种模式之长,我们来看看用这个方式重写的上面的例子:
1 function Person(name,age,job){
2 this.name=name;
3 this.age=age;
4 this.job=job;
5 this.friends=["zhangsan","lisi"];
6 }
7 Person.prototype={
8 constructor:Person,
9 sayname: function(){
10 alert(this.name);
11 }
12 }
13
14 var person1=new Person("Sleipnir",23,"Software");
15 var person2=new Person("xiaoxiao",25,"Student");
16
17 person1.friends.push("van");
18 console.log(person1.friends); //"zhangsan,lisi,van"
19 console.log(person2.friends); //"zhangsan,lisi"
20 console.log(person1.fgriends===person2.friends); //false
21 console.log(person1.sayname===person2.sayname); //true
这种构造模式和原型混合的模式,是目前使用最多的一种方法,可以说,这是用来定义引用类型的一种默认模式。
5.动态原型模式
这种模式是解决了在构造器中查看并初始化原型的问题,它的动态性在于,在构造器中检查一下原型中是否存在某个方法,如果不存在,就建立下,建立一次就行了,一劳永逸,这就是其方便之处,我们同样来看个例子:
1 function Person(name,age,job){
2 this.name=name;
3 this.age=age;
4 this.job=job;
5
6 //检查原型中是否有方法
7 if(typeof this.sayname!="function"){
8 Person.prototype.sayname=function(){
9 alert(this.name);
10 };
11 }
12 }