这篇文章主要是根据JavaScript高级程序设计第6章总结一下JavaScript创建对象的几种方法
创建Object实例
var o=new Object();
o.name='John';
o.age=15;
o.job='Engineer';
o.sayName=function(){
alert(this.name);
}
这样子每次添加一个属性或者方法都要写一次对象名,很麻烦,于是就有下面这种方法。
对象字面量
var o={
name:'John',
age:15,
job:'Engineer',
sayName:function(){
alert(this.name);
}
}
事实上,用上面的两种方法来创建对象都很不方便,除非只是创建单个对象,但是如果要创建大量类似的对象,这个过程则会产生大量的重复代码,所以就发明了一种函数,用函数来封装以特定接口创建对象的细节,也就是抽象化创建具体对象的过程然后用函数封装起来,这种设计模式就叫工厂模式。
工厂模式
function createPerson(name,age,job){
var o=new Object();
o.name=name;
o.age=age;
o.job=job;
o.sayName=function(){
alert(this.name);
}
}
var person1=createPerson('John',15,'Engineer');
var person2=createPerson('Mike',15,'Student');
工厂模式虽然解决了创建多个相似对象的问题,但是没有解决对象识别的问题,即无法知道一个对象的类型。
构造函数模式
function Person(name,age,job){
this.name=name;
this.age=age;
this.job=job;
this.sayName=function(){
alert(this.name);
}
}
var person1=new Person('John',15,'Engineer');
var person2=new Person('Mike',15,'Student');
构造函数与工厂模式不同之处:
- 没有显示创建对象
- 直接将属性和方法赋给this对象
- 没有return语句
而且为了区分构造函数和普通函数,构造函数函数名首字母大写。
要创建Person的新实例,必须使用new
操作符,且会经历下面4个步骤:
(1) 创建一个新对象
(2) 将构造函数的作用域赋给新对象
(3) 执行构造函数中的代码
(4) 返回新对象
而且构造函数创建的对象都有一个constructor(构造函数)属性,它默认值指向它的构造函数,例如
alert(person1.constructor==Person);//true
alert(person1.constructor==Object);//false
constructor最初是用来标识对象类型的,但是,提到检测对象类型,还是instanceof更可靠一些。这个例子中创建的对象既是Obejct的实例,又是Person的实例,而这一点可以通过instanceof证实。
alert(person1 instanceof Object);//true
alert(person1 instanceof Person);//true
这便解决了对象识别的问题。
扩展:
任何函数,只要通过new操作符来调用,那它就是作为构造函数,否则,就是普通函数。
Person('Greg',27,'Doctor');
window.sayName();//'Greg'
window.age;//27
作为普通函数调用时,this的作用域为window对象,因此属性和方法都添加到window对象中去了。我们也可以在其他对象的作用域中调用:
var o=new Object();
Person.call(o,'Susan',22,'Nurse');
o.sayName();//'Susan'
扩展结束
尽管构造函数解决了对象识别的问题,但是,它也还存在问题,就是每个方法都在每个实例中重新创建了一遍,但这些方法都是一样的,没有必要每创建一个实例就创建一次,这样很重复。可以把方法提到构造函数外:
function Person(name,age,job){
this.name=name;
this.age=age;
this.job=job;
this.sayName=sayName;
}
function sayName(){
alert(this.name);
}
这样子,方法就可以被实例所共享使用了,但是却是只能被这个构造函数的实例对象所使用,而方法却是全局方法,这使得全局作用域有点名不副实。而且,如果对象需要定义很多方法,就要定义很多个全局函数,这就没有了“封装性”特点。而这个问题可以通过使用原型模型来解决。
原型模式
我们创建的每一个函数都有一个prototype属性,它是一个指针,指向一个对象,这个对象包含了该函数实例所共享的属性和方法。也就是说,我们可以把构造函数中的属性和方法都放在它的原型对象中去,那么它的每一个实例就都可以访问了。
function Person(){
}
Person.prototype.name="John";
Person.prototype.age=15;
Person.prototype.job="Engineer";
Person.prototype.sayName=function(){
alert(this.name);
}
var person1=new Person();
var person2=new Person();
person1.sayName();//'John'
person2.sayName();//'John'
这样就解决了无法共享方法的问题,但是!!原型对象中的所有方法和属性都是共享的,也就是说,每一个实例对象在原型对象中获得的属性值都是一样的,有时候这并不是我们想要的,我们希望我们创建的实例对象有自己的特点,而不是一模一样,这样没有意义,因此,我们应该组合起来使用。
扩展:
- 每次创建一个新函数,就会自动创建一个prototype属性,这个属性指向该函数的原型对象,而原型对象会自动获得一个constructor属性,这个属性指向prototype属性所在的函数。但是如果重写原型对象,constructor值会改变。
Person.prototype.constructor===Person;//true
- 当调用构造函数创建一个新实例后,该实例的内部会包含一个属性[[prototype]],指向构造函数的原型对象。
- 使用isPrototypeOf()方法可以确定参数对象的原型是否为调用对象
Person.prototype.isPrototypeOf(person1);//true;
- 使用Object.getPrototypeOf()可以获得对象的原型
Object.getPrototypeOf(person1)==Person.prototype;//true
Object.getPrototypeOf(person1).name;//'John'
- 每次代码读取对象的属性时,首先在对象实例中查找,找到则返回,找不到则到原型对象中去查找,找到则返回,否则,如果该构造函数继承了其他函数,则继续往上面的原型对象中查找。具体到继承中再继续讲解。
- 实例可以访问原型对象中的值,却不能重写原型中的值,只会在实例对象中创建一个与原型对象同名的属性屏蔽掉它。
function Person(){
}
Person.prototype.name="John";
Person.prototype.age=15;
Person.prototype.job="Engineer";
Person.prototype.sayName=function(){
alert(this.name);
}
var person1=new Person();
var person2=new Person();
person1.name='zcn';//创建了同名属性,屏蔽了原型对象的同名属性
alert(person1.name);//'zcn'——来自实例
alert(person2.name);//'John'——来自原型
也就是只会阻止我们访问原型中的属性,但不会修改原型中的属性。我们可以用delete完全删除实例属性,从而重新访问到原型中的属性。
delete person1.name;
alert(person1.name);//'John'——来自原型
- 一直用Person.prototype来添加属性和方法很累赘,其实可以使用对象字面量来重写整个原型对象。
function Person(){
}
Person.prototype={
name:'John',
age:29,
job:'Engineer',
sayName:function(){
alert(this.name);
}
};
但是这样子会改变constructor的值,使它指向了Object,因为重写之后,实际上是Object构造函数造了它。但instanceof仍然返回之前的结果,只是constructor不能确定对象的类型了。
var friend=new Person();
alert(friend instanceof Object);//true
alert(friend instanceof Person);//true
alert(friend.constructor==Person);//false
alert(friend.constructor==Object);//true
因为它是可配置的,所以如果需要,我们可以把它改回来。
function Person(){
}
Person.prototype={
constructor:Person;//这种方法创建,三大特性默认值为true
name:'John',
age:29,
job:'Engineer',
sayName:function(){
alert(this.name);
}
};
重设constructor值之后,它的特性[[Enumetable]]被设置成true,默认是false,我们可以把它改回来。
function Person(){
}
Person.prototype={
name:'John',
age:29,
job:'Engineer',
sayName:function(){
alert(this.name);
}
};
Object.defineProperty(person.prototype,'constructor',{
enumerable:false,//该方法定义三大特性默认为false,此处不写也可以
value:Person
});
原型的动态性,我们可以随时给原型对象添加新的属性和方法,而且都可以在实例中反映出来, 不管是先创建实例还是先添加属性方法,因为实例的[[prototype]]是一个指针。而如果是重写原型对象,则不会影响在这重写之前的创建的对象,只会反映在重写之后创建的对象中。
扩展结束
组合使用构造函数模式和原型模式
function Person(name,age,job){
this.name=name;
this.age=age;
this.job=job;
}
Person.prototype.sayName=function(){
alert(this.name);
}
var person1=new Person('John',15,'Engineer');
var person2=new Person('Mike',21,'Student');
person1.sayName();//'John'
person2.sayName();//'Mike'
person1.sayName===person2.sayName;//true
如此以来,每个实例对象既有自己的一份实例属性的副本,又同时共享着对原型对象中共享属性和方法的引用。
这种模式是目前在ES中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。
动态原型模式
独立的构造函数和原型,看起来有点奇怪,动态原型模式便将所有信息都封装在构造函数中,而通过检查任意一个应该存在的方法是否有效,来决定是否要初始化原型。
function Person(name,age,job){
//属性
this.name=name;
this.age=age;
this.job=job;
if(typeof this.sayName != 'function'){
//if(!this.sayName instanceof Function){
//初始化原型,此处必须使用Person,this的作用域是实例,而实例是无法给prototype增加方法或属性
Person.prototype.sayName=function(){
alert(this.name);
}
}
}
附加
寄生构造函数模式
在上述几种模式都不适用的情况下,可以用这种模式,比如我们想创建一个具有额外方法的数组,又由于我们无法直接修改Array的构造函数,此时可使用这种模式。
function SpecialArray(){
var arr=new Array();
//
arr.push.apply(arr,arguments);
arr.toPipedString=function(){
return this.join("|");
};
return arr;
}
var colors=new SpecialArray('red','blue');
colors.toPipedString();//"red|blue"
但这种模式只是封装了创建对象的代码,它创建的对象与该构造函数是没有任何关系的,因此也就不能用instanceof来确定对象类型,所以,在上述模式可以使用的情况下,不建议使用这种模式。
稳妥构造函数模式
这种模式与上一种模式类似,它是用于创建稳妥对象的一种模式,稳妥对象就是没有公共属性,它的方法也不引用this的对象,而且创建时并不使用new,适用于安全环境中。
function Person(name,age,job){
var o=new Object();
o.name=name;
o.age=age;
o.job=job;
o.sayName=function(){
alert(name);
}
return o;
}
var p=Person('John',28,'Engineer');
p.sayName();//'John'
这种模式创建的对象,属性值只能是通过调用特定的方法来访问,比如例子中的name只能是sayName()来访问,即使添加其他方法,也不可能有别的方法访问传入到构造函数中的原始数据。与寄生构造函数类似,instanceof对于这种对象也没有意义。
总结
总结一下思路,创建Object构造方法的实例或者对象字面量的方法,创建多个相似对象时代码重复,于是采用工厂模式,工厂模式又不能进行对象识别,于是有了构造函数,构造函数不能共享函数,每一个对象都重新创建了功能一样的方法,很占用空间也很多余,于是出现了原型模式,原型模式中的属性和方法都是被共享的,每一个对象实例拥有的属性值都一致,并不能完全令我们满意,于是就有了组合使用构造函数和原型模式,这样子每一个对象既有自己的属性,又能访问到原型对象中共享的(属性和)方法。最后,还可以把原型模式写入到构造函数中,即动态构造函数。
最后附加了两种特殊的模式,寄生构造函数模式和稳妥构造函数模式,寄生构造函数模式是其他模式都不能用的情况下再考虑使用,而稳妥构造函数模式则是在安全环境中,或者防止数据被其他应用程序改动时使用。这两种模式创建的对象都跟构造函数没有关系。
这就是创建对象的九种模式,如果文中有什么表达错误的地方,希望大家指出来,谢谢大家。