虽然我们可以通过Object构造函数或者字面量的方式创建单个对象,但它有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。
接下来我们来讲解一下创建对象的四种方式。
1、工厂模式
工厂模式是一种非常重要的设计模式,有很多实际应用。在ECMAScript中无法创建类,但可以通过工厂模式来抽象具体对象的创建过程,用工厂函数封装以特定接口创建对象的细节 。
function createPerson(name,age,sex){
var o=new Object();
o.name=name;
o.age=age;
o.sex=sex;
o.sayName=function(){
alert(o.name);
}
return o;
}
var p1=createPerson("rabbit",18,"female");
工厂模式做到了能够批量生成许多相识的对象,但是仍然没有“类”的概念,无法鉴别对象的类型。
2、构造函数模式
ECMAScript的构造函数可用来创建特定类型的对象。像Object、String、Array等都是。
function Person(name,age,sex){
this.name=name;
this.age=age;
this.sex=sex;
this.sayName=function(){
console.log(this.name);
}
}
var p=new Person("rabbit",18,"female");
console.log(p instanceof Object); //true
console.log(p instanceof Person); //true
console.log(p.constructor); //打印出其构造函数的定义
通过构造函数创建的对象有了类的概念,可以使用 instanceof检测
可以看出与工厂模式的不同之处:
1、没有显式的创建对象(后台会自动创建)
2、直接将参数赋给了this
3、没有return (后台自动返回)
4、通过 new 来创建对象
用 new 操作符调用构造函数会经历以下四个步骤:
1、创建一个新对象
2、将构造函数的作用域(执行环境)赋给新对象(this就指向了这个新对象)
3、执行构造函数中的代码(给新对象添加属性函数啥的)
4、返回这个新对象
通过构造函数创建的对象实例有一个constructor属性(默认不可枚举),该属性指向构造函数。
构造函数和普通函数的不同之处就是调用方式不同,但构造函数也是函数,也可像普通函数那样使用。任何函数只要通过new来调用,都可成为构造函数。看下面的例子:
//构造函数同上
var p1=new Person("Rabbit",18,"female");
Person("Lisi",30,"male"); //当做普通函数来调用,那么构造函数中的this指的就是window对象
window.sayName(); //返回 "Rabbit"
var o=new Object();
Person.call(o,"ZhangSan",20,"female");
o.sayName(); //返回 "ZhangSan"
也可以在使用call某个特殊的作用域下调用Person函数,于是this就指向了对象o。
构造函数的问题:
虽然构造函数能够简单的创建多个类似的对象,并且有了类的概念。但是我们发现每个方法都要在每个实例上创建一遍。
在ECMAScript中,函数也属于对象,即每创建一个函数都相当于实例化了一个对象。那上面的构造函数也可表示为:
function Person(name,age,sex){
this.name=name;
this.age=age;
this.sex=sex;
this.sayName=new Function("console.log(this.name)");
//不难看出,每个Person实例都包含一个Function实例
}
可是,我们没有必要创建两个能够完成相同任务的函数,何况这个函数已经使用了this来绑定不同的实例。以sayName()函为例,我们只需要创建一个实例即可,谁调用这个函数,this就指向谁,就输出谁的名字。同样能完成任务,为什么要创建那么多的实例呢。
基于这种思想,后来就有了下面的方法:
function Person(name,age,sex){
this.name=name;
this.age=age;
this.sex=sex;
this.sayName=sayName;
}
function sayName(){
console.log(this.name);
}
这样,我们只创建了一个sayName实例,达到了上面的那种效果。但这种方法也不好。
如果我们有多个像sayName这样的函数,那么我们岂不是要在全局作用域下声明那么多个函数,毫无封装性可言。
于是出现了下面的第三种方法。
3、原型模式
我们创建的每一个函数都有一个prototype的属性,该属性是一个指针,指向一个对象(原型对象),该对象包含了由特定类型的所有实例共享的属性和方法。 共享!!!!
这就意味着我们可以不用在构造函数中定义属性和方法了,直接定义一个空的构造函数,然后在构造函数外部给原型对象添加属性和方法
function Person(){}
Person.prototype.name="Rabbit";
Person.prototype.age="18";
Person.prototype.sex="male";
Person.prototype.sayName=function(){alert(this.name);}
var p1=new Person(); //p1.name是"Rabbit"
var p2=new Person(); //p2.name也是"Rabbit"
理解原型对象:
创建一个函数(无论是函数声明还是函数表达式),就会生成一个prototype属性,该属性指向一个原型对象。默认情况下原型对象只会有一个constructor属性,该属性是一个指针,指向prototype属性所在的构造函数。当用构造函数创建了一个对象实例后,该实例内部将包含一个指针,指向构造函数的原型对象。在谷歌、火狐、safari浏览器中这个指针被叫做__proto__,可在控制台中输入一个对象实例来发现这个指针。这样,构造函数和原型对象之间通过prototype属性和constructor属性连接起来,对象实例和原型对象之间通过__proto__属性连接起来,而构造函数和对象实例之间没有直接的联系。
isPrototypeOf( ) 确定对象实例和原型对象之间的关系
Object.getPrototypeOf( ) 获得某个对象实例的原型对象
console.log( Person.prototype.isPrototypeOf( p1 ) ); //返回true
var proto=Object.getPrototypeOf(p1);
console.log(proto.name); //返回“Rabbit”
每当代码读取对象实例的某个属性的时候,会先去对象实例里寻找是否有这个属性,如果有则返回,若没有则去对象实例的原型中去寻找。这就是对象实例共享原型中属性和方法的原理。
我们能够通过对象实例去访问原型对象中的属性,却不可以修改(重写)原型对象中的属性。例如:
function Person(){}
Person.prototype.name="Rabbit";
Person.prototype.age="18";
Person.prototype.sex="male";
Person.prototype.sayName=function(){alert(this.name);}
var p1=new Person();
p1.name="Arch";
console.log(p1.name); //返回"Arch"
var p2=new Person();
console.log(p2.name); //返回"Rabbit"
从上面的结果可见,p1.name=“Arch” 只是给p1这个实例设置了name属性,其原型中的name属性还在,只是被p1给屏蔽了而已,通过p2.name仍然能访问到原型中的name属性。所以说只能屏蔽,不能修改。即使将p1.name=null也不能恢复成原型对象的name属性,但是通过delete p1.name删除实例中的name属性来恢复至原型中的name属性。
hasOwnProperty( prop ) 查看属性在实例中还是在原型中,在实例中则返回true。p1.hasOwnProperty(name) //返回false
in操作符:
console.log( “name” in p1); //只要name属性在p1对象中就会返回true,无论name是实例属性还是原型属性。
所以使用 in 和 hasOwnProperty( ) 就能够确定属性是在实例中还是在原型中。
for-in 可遍历所有可枚举的属性,包括实例属性和原型属性。
Object.keys( ) 可获得对象上所有的可枚举的实例属性。
Object.getOwnPropertyNames( ) 获取对象上所有的实例属性,不管是否可枚举。
更为简单的原型写法:不用重复的写多次Person.prototype
function Person(){}
var old=Person.prototype;
Person.prototype={
//constructor:Person, 如果想Person.prototype.constructor再次指向Person,只能强行加入
name:"Rabbit",
age:18,
sex:"male",
sayName:function(){alert(this.name);}
}
console.log(old.constructor); //指向Person
console.log(Person.prototype.constructor); //指向Object
但是,这相当于重写了原型对象。于是就是失去了构造函数和原型对象之间的关系。Person的原型对象依然存在,其中的constructor属性仍然指向Person的构造函数。只是Person.prototype不再指向Person了。新的原型对象依然有constructor属性,不过它指向Object构造函数。新的原型对象实际上就是Object对象的一个实例,所以新的原型对象的constructor指向Object。
强行指向的constructor属性的 [[Enumerable]] 特性 会被设置为true,可以使用Object.defineProperty( ) 修改回去。
原型的动态性:
function Person(){}
Person.prototype.name="Rabbit";
Person.prototype.age="18";
Person.prototype.sex="male";
Person.prototype.sayName=function(){alert(this.name);}
var p1=new Person();
Person.prototype.sayHi=function(){
alert("Hi!");
}
p1.sayHi(); //弹出"Hi!"
当p1调用sayHi()函数时,会先在实例中寻找这个函数,找不到才回去原型中找,在原型中找到即调用。
然而如果像上面那样重写了原型对象:
function Person(){}
var p1=new Person();
Person.prototype={
name:"Rabbit",
age:18,
sex:"male",
sayName:function(){alert(this.name);}
}
p1.sayName(); //报错
因为p1的__proto__属性仍然指向原来的原型对象,原来的原型对象并没有sayName()函数。
原型对象的问题:
原型最大的特点就是它能够共享,但这也是它的缺点。共享对于那些需要共享的函数来说确实很方便,但是一个对象总有些不用共享的属性和函数。对于基本类型的属性来说,这种共享还没太大的问题。p1.name="aaa",是在设置p1的实例属性,不会影响到p2的name值。但是对于那些引用类型的属性来说就显得比较麻烦了。
function Person(){}
var p1=new Person();
Person.prototype={
constructor:Person,
name:"Rabbit",
age:18,
sex:"male",
friends:["zs","ls"],
sayName:function(){alert(this.name);}
}
var p1=new Person();
var p2=new Person();
p1.friends.push("ww");
console.log(p1.friends); //["zs","ls","ww"]
console.log(p2.friends); //["zs","ls","ww"]
每个人都有不同的friends,但事实却是这个属性被共享了,并且p1的修改会影响到p2。
一般的实例都会有自己的实例属性,这些属性是不能共享的。所以单独使用原型模式达不到这种效果。
4、组合使用构造函数和原型模式
在构造函数中定义实例属性和方法,在原型中定义共享的属性和方法。
function Person(name,age,sex,friends){
this.name=name;
this.age=age;
this.sex=sex;
this.friends=friends;
}
Person.prototype={
constructor:Person,
sayName:function(){
alert(this.name);
}
}
这样,每个实例既有自己独立的实例属性,又有共享的函数方法。
可能会有人觉得这么分开写会有些奇怪,于是又出现了动态原型模式:
function Person(name,age,sex,friends){
this.name=name;
this.age=age;
this.sex=sex;
this.friends=friends;
if(typeof Person.prototype.sayName != "function" ){ //也可以写成this.sayName
Person.prototype.sayName=function(){alert(this.name);}
Person.prototype.sayHi=function(){alert("Hi");}
.....
}
}
在创建第一个对象实例时,就将原型方法创建好,以后再创建其他对象实例的时候就不用创建了。
这样结合了构造函数和原型模式的优点,而且看起来也不会太怪。