一、创建对象
object构造函数或者对象字面量都可以用来创单个对象,但是创建多个对象的时候会与大量的重复代码。以下通过分别介绍工厂模式、构造函数模式、原型模式、构造函数模式与原型模式的组合
1、工厂模式
先直接上代码:
function createPerson(name,age){
var o = new Object();
o.name =name;
o.age=age;
o.sayName =function (){
alert(this.name);
};
return o;
}
var person1 = createPerson("周运金","20");
person1.sayName(); //周运金
工厂模式应该是最容易想到的,通过函数来封装一个对象然后返回。体现了一种接口模式。缺点在于无法识别对象,就是实例不知道“”爸爸“”是谁啊。这就需要构造函数来解决了。
2、构造函数模式
先来看代码
function Person(name,age){ //首字母大写
this.name =name;
this.age=age;
this.sayName =function(){
alert(this.name);
};
}
person1 = new Person("李佳家","20");
person2=new Person("周运金","20");
person1.sayName(); //李佳家
person2.sayName(); //周运金
你会不会觉得很奇怪,在函数前面加了个new 就像是c++里面利用构造函数创建一个对象。没错,在这里它就是一个构造函数。在js中当函数前面没有加new是按照普通的函数执行。在使用了new之后就将该函数当成是构造函数。
但是即使是这样this指向的貌似是window对象。当把该函数当做普通函数使用时,这个this确实指向了window。但是使用了new就不同了,这个要从构造函数调用过程讲起了,调用构造函数会经历四个步骤:
i.创建一个新的对象。
ii.将构造函数作用域赋给刚创立的对象(此时this指向了刚刚创立的对象)
iii.执行构造函数的代码
iii.将新的对象赋值给左值(例如person1)
如果不能理解的话可以直接理解为c++中的调用构造函数实例化一个对象。
可以用instanceof找“”爸爸“”了
alert(person1 instanceof Person) //true
alert(person1 instanceof Object) //true
在之前的文章中我们用instanceof来查看引用类型,这里再一次验证了所有的对象都继承了Object对象。
但是构造函数有一个问题:看下面的代码
alert(person1,sayName==person2.sayName); //false
我们知道在c++当中,其实类里面的函数是共享内存的,在这里完成同样的任务,没有必要实例化出两个不同的function。虽然可以把function放到全局环境中,然后用this.sayName=sayName,这种方式来解决这个问题,但是这个函数所有“人”都可以用了,那何来的封装呢。所以才有了原型模式。
3、原型模式
先来一段原型模式的代码:
function Person(){
}
Person.prototype.name="周运金";
Person.prototype.sayName=function (){
alert(this.name);
}
var person1 =new Person();
person1.sayName(); //周运金
var person2 =new Person();
person2.sayName(); //周运金
alert(person1.sayName==person2.sayName); //true
akert(Person.prototype.isPrototypeOf(person1)); //true
在理解原型模式之前先理解一下原型对象是什么,以及构造函数、实例、原型对象三者之间的关系。
什么是原型对象?
在js中,每一个函数都会有一个属性叫做prototype(原型)的指针,该指针指向的就是这个函数的原型对象。这个原型对象会有一个默认属性叫做constructor,这个属性又指向了所在函数的指针。看图:
当实例化一个person对象person1之后,person1会有一个属性[[Prototype]]指向了person的原型对象。可以看出实例只是指向了构造函数的原型对象,没有直接指向构造函数,这个需要注意。
现在再来理解一下代码:
上面的代码中所有的属性和函数都没有在构造函数中实现,而是直接将属性和函数添加到原型对象中。(Person.prototype其实是原型对象),这样做的目的是让实例(person1)能够共享属相和方法.
原型对象有一个方法isPrototypeOf来测试实例的原型对象。
person 实例在访问sayName方法的时候其实有一次搜索的过程,它先搜索一下实例有没有这个方法,有的话就立即停止搜索并返回,如果没有的话才去原型里面查找有没有这个方法,换句话说,实例的属性或者方法,会屏蔽原型的同名属性或者方法。这个应该很好理解就不上代码了。
对于可以访问的属性来说,可以采用for in遍历,也可以单独用in来判断实例或者原型有没有相应的属性,还可以用Object.keys()来获取所有的可以访问的属性:
接着上面的代码:
alert("name" in person1); //true
alert("age" in person2); //false
for(var v in person1)
{
alert(v); //分别打印name/sayName
}
var keys =Object.keys(Person.prototype); //name,sayName
alert(keys);
alert(Object.keys(person1)); //空
需要注意的几个问题:
1、指针仅指向原型,不指向构造函数
function Person(){
}
var friend =new Person();
Person.prototype ={
constructor:Person,
name:"周运金",
sayName:function(){
alert(this.name);
}
}
friend.sayName(); //error
在代码中我们先实例化一个对象friend,然后重写了原型对象,再调用friend.sayName报错。原因是找不到该方法。用一个图来说明这个问题:
从图上可以看出,friend还是在引用最初的原型。
共享属性的一个弊端
我们知道,在c++当中,函数是共享内存的,但是属性却是独立的,每个实例都希望有自己的实例,别的实例化对象不能改变我的属性值,在原型中,无论是函数还是属性都是共享的。特别是对于引用类型的属性。比如:
function Person(){
}
Person.prototype ={
constructor:Person,
name:"周运金",
friends:["周运金","李佳家"],
sayName:function(){
alert(this.name);
}
}
var person1 =new Person();
var person2 =new Person();
person1.friends.push("啊妹");
alert(person1.friends); //周运金,李佳家,阿妹
alert(person2.friends); //周运金,李佳家,阿妹
alert(person1.friends==person2.friends) //true
从上面代码可以看出person1会影响person2的属性。从最后一段代码可以发现,他们是相等的,这就gg了。
回忆一下之前构造函数的特点,构造函数里面定义的属性在实例之间是不相等的。那么很自热的想到了一种组合的方式。
4、构造函数模式与原型模式的组合
根据上面的提示,可以将实例不共享的属性放在构造函数定义,共享都属性和方法用原型模式定义。
function Person(name)
{
this.name =name;
this.friends=["周运金","李佳家"];
}
Person.prototype ={
constructor:Person,
sayName:function(){
alert(this.name);
}
}
var person1 =new Person("周运金");
var person2 =new Person("李佳家");
person1.friends.push("阿妹");
alert(person1.friends); //周运金,李佳家,啊妹
alert(person2.friends); //周运金,李佳家
alert(person1.friends==person2.friends); //false
alert(person1.sayName ===person2.sayName); //true
分析以上代码可知,上面的问题果然可以解决了,这也是运用最广泛的自定义类型的方法。
原型链继承
先开看一段原型链继承的代码:
function SuperType(){
this.property =true;
}
SuperType.prototype.getSuperValue =function (){
return this.property;
}
function SubType(){
this.subproperty =false;
}
//继承SuperType
SubType.prototype =new SuperType();
SubType.prototype.getSubValue =function(){
return this.subproperty;
}
//实例化一个对象insatance
var instance =new SubType();
alert(instance.getSuperValue()); //true
从上面的代码可以看出,用SubType实例化的对象instance可以访问SuperType的方法。来看一下代码执行之后的关系:
instance的[[Prototype]]指向SubType的原型对象,SubType的原型对象的[[Prototype]]指向SuperType的原型对象,就这样一级一级往上,一直到Object的原型对象,这就是原型链继承的原理了。还是可以用intanceof来找“爸爸”的。
与c++的类类似,可以为实例添加新的方法,也可以重写超类的方法。但是新添加的方法一定要在重写方法的前面,还有不能用字面量创建原型方法,这样会重写原型链,导致先实例化的对象失效。这个由上面所说的实例指向的是原型可以解释,重写原型之后之前的实例还是指向最初那个原型对象,这样就会切断原型链。
原型链的问题
1、其实原型链的问题又回到了共享属性上了,因为在讲原型模型的时候会遇到共享属性的问题,当时是用构造函数解决的,将不共享的属性放在构造函数里面,但是通过原型链继承的时候又暴露出来了,当在继承时,是将超类的一个实例复制给原型,那么这个原型就很自然的拥有了超类实例对象的属性了,我们知道在原型中的属性是会被所有实例所共享的,比如说一个原型的引用类型属性,一个实例对象改变了这个引用类型的话就会影响到所有的实例对象。比如:
function SuperType(){
this.colors = ["red","blue","green"];
}
function SubType(){
}
//继承SuperType
SubType.prototype =new SuperType();
//实例化一个对象insatance
var instance1 =new SubType();
var instance2 =new SubType();
instance1.colors.push("black");
alert(instance1.colors); //red,blue,green,black
alert(instance2.colors); //red,blue,green,black
2、由上面的分析可知,无法在不影响所有对象实例的情况下给超类的构造函数传递参数,因此很少单独使用原型链来继承。
解决办法:采用借用构造函数
既然问题出现在共享上面,那么就在将要创建实例的构造函数里面执行超类的构造函数,如此一来在将来要实例化对象的时候在新的对象上执行超类中定义的所有对象初始化代码。结果每个实例都会有自己(超类中的)的副本。同时还可以给超类传递参数比如:
function SuperType(name){
this.name=name;
this.colors = ["red","blue","green"];
}
function SubType(){
//继承SuperType
SuperType.call(this,"李佳家");
//子类属性
this.age =20;
}
//实例化一个对象insatance
var instance1 =new SubType();
var instance2 =new SubType();
instance1.colors.push("black");
alert(instance1.name); //李佳家
alert(instance1.colors); //red,blue,green,black
alert(instance2.colors); //red,blue,green
console.log(instance2);
console.log(instance2)
但是借用构造函数还是有问题的。在之前说构造函数的问题的时候就提到过,函数如果定义在构造函数里面,那么就没有复用的优点了。所以该单独使用借用构造函数还是不行的。
3、组合继承
既然原型链和借用构造函数一般都不单独使用,那么就整合两者的优缺点。思路就是,用原型链实现对原型属性的方法复用,用借用构造函数来实现对实例属性的继承。
function SuperType(name){
this.name =name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName=function(){
alert(this.name);
}
function SubType(name,age){
//继承SuperType属性
SuperType.call(this,name);
this.age =age;
}
//继承方法
SubType.prototype =new SuperType();
SubType.prototype.constructor =SubType;
SubType.prototype.sayAge =function (){
alert(this.age);
}
//实例化一个对象insatance
var instance1 =new SubType("周运金",20);
instance1.colors.push("black");
alert(instance1.colors); //red,blue,green,black
instance1.sayName(); //周运金
instance1.sayAge(); //20
var instance2 =new SubType("李佳家",21);
alert(instance1.colors); //red,blue,green
instance1.sayName(); //李佳家
instance1.sayAge(); //21
console.log(instance2);
console.log(instance2)
这不是觉得很棒。这也是js最常用的继承模式了。好好体会吧。