Javascript教程:什么是原型对象
在此,我们将sayName()方法和所有属性直接添加了Person的prototype属性中,构造函数变成了空函数。即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1和person2访问的都是同一组属性和同一个sayName()函数。要理解原型模式的工作原理,必须理解ECMAScript中原型对象的性质;
如何理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。就拿前面的例子来说,Person.prototype.constructor指向Person。而通过这个构造函数,我们还可以继续为原型对象添加其它属性和方法;
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其它方法,则都是从Object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。在很多实现中,这个内部属性的名字是_proto_,而且通过脚本可以访问到(在firefox、Safari、Chrome和Flash的ActionScript中,都可以通过脚本访问_proto_);而在其它实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点,就是这个连接存在与实例与构造函数的原型对象之间,而不是存在于实例于构造函数之间;
虽然在某些实现中无法访问到内部的_proto_属性,但在所有实现中都可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。从本质上来讲,如果对象的_proto_指向调用isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回true,如下所示:
这里,我们用原型对象的isPrototypeOf()方法测试了person1和person2。因为它们内部都有一个指向Person.prototype的指针,因此都返回了true;
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。也就是说,在我们调用person1.sayName()的时候,会先后执行两次搜索。首先,解析器会问:“实例person1有sayName属性吗?”答:“没有。”然后,它继续搜索,再问:“person1的原型有sayName属性吗?”答:“有。”于是,它就读取那个保存在原型对象中的函数。当我们调用person2.sayName()时,将会重现相同的的搜索过程,得到相同的结果。而这正是多个对象共享原型所保存的属性和方法的基本原理;
前面提到过,原型最初值包含constructor属性,而该属性也是共享的,因此可以通过对象实例访问;
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。来看下面的例子:
在这个例子中,person1的name被一个新值给屏蔽了。但无论访问person1.name还是访问person2.name都能正常地放回值,即分别是“Greg”(来自对象实例)和“Nicholas”(来自原型)。当在alert()中访问person1.name时,需要读取它的值,因此就会在这个实例上搜索一个名为Name的属性。这个属性确实存在,于是就返回它的值而不必再搜索原型了。当以同样的方式访问person2.name时,并没有在实例上发现该属性,因此就会继续搜索原型,结果在那里找到了name属性;
当为对象实例添加了一个属性时,这个属性就会屏蔽原型对象汇总保存的同名属性;换句话说,添加这个属性只会组织我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性值设置为null,也只会在实例中设置这个属性。而不会回复其指向原型的连接。不会,使用delete操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性,如下所示: