在js中我们创建的每一个函数都有一个prototype(原型)属性,这个属性其实就是一个指针,它指向一个对象,而这个对象的用途是包含可以有特定的所有实例共享的属性和方法。从字面意思来说,prototype就是通过调用构造函数的那个对象实例的原型对象。
如下面的例子1所示:
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age="29";
Person.prototype.job="Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName);
在这个例子中,我们不需要在构造函数中定义对象实例的信息,直接将信息添加到了原型对象中,这里的构造函数为空构造函数了。后面还会为大家介绍一种简单的原型语法。
要理解原型模式,我们必须要理解什么是原型对象。
一、
任何时候,只要我们创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象,而在默认的情况下,所有的原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在的函数的指针。
如图1所示:
图直接用的word画的,很糙。
我们来分析下这个图。
图中Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。Person的实例person1和person2都包含了一个内部属性,该属性仅仅指向了Person.prototype,与构造函数没有直接的关系。
虽然这两个实例中没有包含属性和方法,但是我们仍然可以调用person1.sayName()。
这要归结于查找对象属性的过程:
先从对象实例本身开始,找到了,ok,返回该属性的值,没有找到,则接着往上找,继续在指针指向的原型对象中找,找到则返回该属性的值。
当我们调用person2.sayName()时也是这一个道理,这体现的是对象实例共享原型所保存的属性和方法的基本原理
虽说我们可以通过实例来访问保存在原型中的值,但是我们不能通过对象实例来重写原型中的值。当你在实例中添加了一个与原型中相同的属性名时,这个属性就会屏蔽掉原型中的那个属性,但是对原型中的属性不会有影响。
我们在代码1的基础上加几行代码再进行分析。
person1.name = "Greg"; //第一行
alert(person1.name); //第二行 “Greg” 来自实例
alert(person2.name); //第三行 "Nicholars" 来自原型
在第一行,我们为person这个实例添加了一个name属性,这个name属性与原型中的name属性同名了,这个时候当我们访问person1.name属性时,得到的结果是"Greg",这时我们屏蔽掉了原型中的name属性。而当我们继续访问person2.name时,发现得到的结果还是"Nicholars",也就是原型中的name并没有被改变。为实例添加同名或是不同名的属性,都是针对于实例来说,与原型无关。
那如果当我们想要重新访问原型中的属性怎么办呢?
使用delete操作符就可以完全删除实例属性了。
例2
delete person1.name;
alert(person1.name); //“Nicholas”来自原型
再说一下原型中的几个方法:
1、hasOwnProperty():检测一个属性是否存在于实例中还是原型中。
举个例子(例3):
person1.name = "BLUE"
alert(person1.hasOwnProperty("name")); //true
alert(person2.hasOwnProperty("name")); //false
从上述代码中我们就可以知道,当一个属性存在于实例中,hasOwnProperty()返回的就是true,否则就是false。
2、in操作符:in操作符不管属性是在实例中,还是在原型中,都会返回true
举个例子(例4):
person1.name = "BLUE";
alert("name" in person1); //true
alert("name" in person2); //true
3、for-in:返回所有能通过对象的访问的、可枚举的属性(原型和实例中)。
举个例子(例5):
var myObject ={
toString :function(){
return "My Object";
}
};
for(var prop in myObjcet){
if(prop =="toString"){
alert("Found toString");
}
}
这里在IE早期的版本中有bug。不会执行alert语句。
二、更简单的原型语法
上面第一种原型语法Person.prototype.name="Nicholars"是不是写起来很繁琐
其实还有一种简单的写法
如例6:
function Person(){}
Person.prototype{
name:"Nicholar",
age:10,
weight:150,
sayName:function(){
alert(this.name);
}
};
这样看起来是不是简单了很多,这相当于将prototype对象重写了,所以原型对象的constructor属性也变成了新对象的construcor属性(这里是指向Object构造函数),不指向Person函数了。当然我们可以在原型属性中添加这样一行代码:
constructor:Person,
这样就能解决了。
三
原型的动态性
原型的动态性指的是我们对原型对象所做的任何修改都能够立即从实例中反映出来,即便是先创建的实例后修改的原型。
举个例子(例7):
function sister = new Person();
Person.prototype.sayHaha = function(){
alert("hahah");
};
sister.sayHaha(); //"hi"
我们是先创建的实例,后修改的原型,为什么sister实例仍然可以访问sayHaha()方法了?这是因为实例和原型之间松散的连接关系。但如果我们将整个原型对象重写了(就是按照例6的原型语法),这时再用sister实例访问sayHaha()方法就会出现错误了。因为重写原型对象时,切断了现有原型与任何之前已经存在的对象实例之间的联系,他们引用的仍然是最开始的原型。
原型模式的缺点:
原型模式的最大问题是有其共享的本性所导致的。
如例8
function Person(){}
Person.prototype = {
constructor:Person,
name:"Ni",
age:10,
job:"teacher"
sister:["xiaohong","xiaoli"],
sayName:function(){
alert(this.name);
}
};
var person1 = new Person1();
var person2 = new Person2();
person1.sister.push("xiaomei");
alert(pseron1.sister); //"xiaohong,xiaoli,xiaomei"
alert(person2.sister); //"xiaohong,xiaoli,xiaomei"
alert(person1.sister = person2.sister); //true
其实我们的本意是希望只有实例person1引用的数组sister中增加一个xiaomer字符串,但结果person2中也增加了。
实际上,我们一般很少单独使用原型模式。