实际上,从上面对原型的讲解来看,原型模式还是有很多问题的,它并没有很好地解决我在第四部分初提出的若干问题:“其一:可以直接识别创建的对象的类型。其二:解决工厂模式解决的创建大量相似对象时产生的代码重复的问题。其三:解决构造函数产生的封装性不好的问题。”其中第一个问题解决的不错,通过构造函数便可以直接看出来类型。第二个问题却解决的不好,因为它省略了为构造函数传递初始化参数这一环节,结果所有的实例在默认情况下都将取得相同的默认值,我们只能通过在实例上添加同名属性来屏蔽原型中的属性,这无疑也会造成代码重复的问题。第三个问题,封装性也还说的过去。因此原型模式算是勉强解决了上述问题。
但是这种方法还由于本身产生了额外的问题。看下面的例子:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(){}
Person.prototype={
constructor:Person,
name:"zzw",
age:21,
school:"xjtu",
friends:["pengnian","zhangqi"],
sayName:function (){
console.log(this.name);
}
};
var person1=new Person();
var person2=new Person();
person1.friends.push("feilong");
console.log(person1.friends);//["pengnian","zhangqi","feilong"]
console.log(person2.friends);//["pengnian","zhangqi","feilong"]
这里我在新建的原型对象中增加了一个数组,于是这个数组会被后面创建的实例所共享,但是person1.friends.push("feilong");这句代码我的意思是添加为person1的朋友而不是person2的朋友,但是在结果中我们可以看到person2的朋友也有了feilong,这就不是我们所希望的了。这也是对于包含引用类型的属性的最大问题。
也正是这个问题和刚刚提到的第二个问题(即它省略了为构造函数传递初始化参数这一环节,结果所有的实例在默认情况下都将取得相同的默认值,我们只能通过在实例上添加同名属性来屏蔽原型中的属性,这无疑也会造成代码重复的问题),很少有人会单单使用原型模式。
第五部分:组合使用自定义构造函数模式和原型模式刚刚我们说到的原型模式存在的两个最大的问题。问题一:由于没有在为构造函数创建对象实例时传递初始化参数,所有的实例在默认情况下获取了相同的默认值。问题二:对于原型对象中包含引用类型的属性,在某一个实例中修改引用类型的值,会牵涉到其他的实例,这不是我们所希望的。而组合使用自定义构造函数模式和原型模式即使构造函数应用于定义实例属性,而原型模式用于定义方法和共享的属性。它能否解决问题呢?下面我们来一探究竟!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
function Person(name,age,school){ this.name=name; this.age=age; this.school=school; this.friends=["pengnian","zhangqi"]; } Person.prototype={ constructor:Person, sayName:function(){ console.log(this.name); } } var person1=new Person("zzw",21,"xjtu"); var person2=new Person("ht",18,"tjut"); person1.friends.push("feilong"); console.log(person1.friends);//["pengnian", "zhangqi", "feilong"] console.log(person2.friends);//["pengnian", "zhangqi"] console.log(person1.sayName==person2.sayName);//true
OK!我们来看看组合使用构造函数模式和原型模式解决的问题:
解决了Object构造函数和对象字面量方法在创建大量对象时造成的代码重复问题(因为只要在创建对象时向构造函数传递参数即可)。
解决了工厂模式产生的无法识别对象类型的问题(因为这里通过构造函数即可获知对象类型)。
解决了自定义构造函数模式封装性较差的问题(这里全部都被封装)。
解决了原型模式的两个问题:所有实例共享相同的属性以及包含引用类型的数组在实例中修改时会影响原型对象中的数组。