大家都知道Javascript是一种基于对象的语言,可以说javascript里一切都是对象,那创建对象都有那些方法呢?
1. 利用Object构造函数或对象字面量创建单个对象
var student1=new Object(); //利用Object构造函数创建对象
student1.name="Amy";
student1.age=10;
var student2={ //利用对象字面量创建对象
name:"John",
age:18
};
console.log(student1.name) //"Amy"
console.log(student2.age) //18
这是我们经常会用到的创建对象的方法,利用这个方法创建单个对象非常方便,但如果我们需要创建多个相似对象,则会产生大量的重复代码。
2.工厂模式
于是,为了改善这个问题,就出现了工厂模式。
function createStudent(name,age) {
var o=new Object();
o.name=name;
o.age=age;
o.sayName=function () {
alert(this.name);
}
return o;
};
var student1=createStudent("Amy",15);
var student2=createStudent("John",18);
alert(student1.name); //"Amy"
alert(student2.name); //"John"
开发人员用函数将用特定接口创建对象的细节封装起来,通过调用函数来创建对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题。
3.构造函数模式
构造函数可以用来创建特定类型的对象。我们可以利用像Object,Array这样的原生构造函数,也可以创建自定义的构造函数。
function Student(name,age) {
this.name=name;
this.age=age;
this.sayName=function () {
alert(this.name);
}
}
var student1=new Student("Amy",12);
var student2=new Student("John",18);
student1.sayName(); //"Amy"
student2.sayName(); //"John"
上述例子使用new操作符创建了Student的实例,以这种方式调用构造函数实际上会经历以下这四个步骤:
1.创建一个新对象;
2.将构造函数的作用域赋给新对象,也就是说,this就指向这个新对象了;
3.执行构造函数中的代码,为这个新对象添加属性;
4.返回这个新对象。
使用new操作符创建Student的实例,每个实例都有一个constructor属性指向构造函数。
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,而这正是构造函数模式胜过工厂模式的地方。
构造函数模式虽然好用,但他并非没有缺点。使用构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍。如果构造函数上存在所有实例共享的属性或者方法,那么,在每个实例上再都重新创建就显的毫无意义。
4.原型模式
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,叫做原型对象,这个原型对象就是用来包含由这个函数所创建的所有实例共享的属性和方法。
function Student(){
}
Student.prototype.name="Amy";
Student.prototype.age=18;
Student.prototype.sayName=function(){
alert(this.name);
}
var student1=new Student();
var student2=new Student();
alert(student1.name) //"Amy"
alert(student2.name) //"Amy"
无论什么时候,只要我们创建了一个新函数,就会自动为这个函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,原型对象都会自动获取一个constructor属性,这个属性是一个指向prototype属性所在函数的指针。
当调用构造函数创建新实例后,每个实例内部都会有一个叫做[[prototype]]的指针,我们叫它隐式原型,它指向构造函数的原型对象。虽然这个指针是实例的内部属性,在脚本中是完全不可见的,但正是因为这个内部属性,我们在实例中才有可能访问到原型对象上的属性和方法,为什么说是有可能,我们后面会解释。
当我们在代码中读取某个对象的某个属性时,我们就会拿着这个属性名,首先在对象实例本身上搜索,如果在对象实例上找到了具有给定名字的属性,则会返回这个属性值,停止搜索。如果没有找到,就会搜索指针指向的原型对象,在原型对象上查找具有给定名字的属性值。如果在原型对象上找到了这个属性,则返回该属性。
比如说在上述代码中。我们要读取student1.name的值,我们首先会在student1当中搜索这个name属性,发现没有,于是我们就去原型对象上进行查找,找到了,并且返回。
但是如果原型对象和对象实例上存在一个同名的属性,那我们读取这个属性的时候,一定取到的是对象实例上的属性值,原型对象上的那个我们压根就不会访问到。比如说下面的代码,我们在实例对象中添加了name属性,那我们在读取name属性的时候,就会取到实例对象中的值。屏蔽掉原型对象中的同名属性。
function Student(){
}
Student.prototype.name="Amy";
Student.prototype.age=18;
Student.prototype.sayName=function(){
alert(this.name);
}
var student1=new Student();
student1.name="John";
var student2=new Student();
alert(student1.name) //"John"
alert(student2.name) //"Amy"
如果这个时候我们修改对象实例中的这个属性,也丝毫不会影响原型对象中的同名属性,即使我们将这个属性设置为null,不过,使用delete则可以完全删除对象实例当中的属性,而让我们可以重新访问到原型对象当中的属性。
使用hasOwnProperty()方法可以检测一个属性是存在于实例当中还是存在于原型中。
我们注意到在给原型对象增加属性时,每次都要敲Student.prototype,非常的麻烦,所以很多时候,我们都会使用对象字面量的方法来给原型对象添加属性。
Student.prototype={
constructor:Student,
name:"Amy",
age:18,
sayName:function(){
alert(this.name)
}
};
但是,我们需要注意的是,用对象字面量的方法给原型对象添加属性,实际上相当于重写了原型对象,这时原型对象当中的constructor属性就不再指向构造函数了,而是指向Object对象。如果我们很需要用到这个constructor,我们可以强制设置constructor属性指向构造函数,就如上面的代码示例所示。
还有一点需要提醒,倘若我们在创建对象实例后又重写了原型对象,那么对象实例和原型对象之间的联系将会被切断,对象实例仍然会指向最开始的原型对象。所以定义在新的原型对象当中的属性对象实例是访问不到的。
原型模式虽然很强大,但它同样也有缺点,首先,原型模式省略了构造函数传递初始化参数这一环节,导致所有对象实例在默认条件下都将取得相同的属性值,某种程度上会带来不便,其次,对于包含引用类型值的属性来说,原型中属性共享会带来很多问题。比如一旦在一个实例上修改引用类型的属性值,原型对象上也会修改。
5.组合使用构造函数模式和原型模式
因为原型模式和构造函数模式都有自己的不足和好处,所以我们通常会组合使用构造函数模式和原型模式,将共享的属性和方法定义在构造函数的原型对象上,将一些引用类型的属性,每个实例都不同的属性定义在构造函数内。
function Student(name,age) {
this.name=name;
this.age=age;
}
Student.prototype.sayName=function(){
alert(this.name);
}
var student1=new Student("Amy",15);
var student2=new Student("John",18);
alert(student1.name) //"Amy"
alert(student2.name) //"John"
student1.sayName() //"Amy"
student2.sayName() //"John"