JavaScript高级程序设计第6章读书笔记(3)

将构造函数当作函数

构造函数与其他函数的唯一区别,就在于调用的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它就跟普通函数没有区别。比如:

构造函数的问题

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。比如,前面的例子中,person1和person2都有一个名为sayName()的方法,但两个方法不是同一个Function实例。

ECMAScript中的函数也是对象,每定义一个函数,也就是实例化了一个对象。因此,构造函数也可以这样定义:

实际上,可以把函数定义转移到构造函数外部来解决这个问题

本例中,person1和person2对象共享了全局作用域中的sayName()函数。但是,问题来了,全局作用域中定义的函数sayName()实际上只能被某个类型的对象(比如:Person类型)调用,这让全局函数有点名不副实。不仅如此,如果对象要定义多个方法,那么就要定义很多个全局函数,这样,我们的自定义引用类型就毫无封装性可言了。

原型模式

我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。也就是说,不必在构造函数中定义对象实例的信息,而是可以直接将这些信息添加到原型对象中,比如:

理解原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。以之前的例子为例, Person.prototype.constructor指向Person。而通过这个构造函数,我们还可以继续为原型对象添加其他属性和方法。

创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来的。当调用构造函数创建一个新实例后,新实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版中管这个指针叫[[Prototype]]。虽然,在脚本中没有标准的方式访问[[Prototype]],但Firefox、Safari和Chrome在每个对象上都支持一个属性__proto__; 而在其他实现中,这个属性对脚本则是完全不可见的。这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。

prototype

如图,Person.prototype指向了原型对象,而Person.prototype.constructor又指向了Person。原型对象中,除了包含constructor属性之外,还包括后来添加的其他属性。Person的每个实例person1、person2都包含一个内部属性,该属性仅仅指向了Person.prototype。也就是说,它们与构造函数没有直接的关系。此外,虽然这两个实例都不包含属性和方法,但我们却可以调用 person1.sayName()。这都是通过查找对象属性的过程来实现的。

虽然在所有实现中都无法访问到[[Prototype]],但,可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回true,如下所示:

本例中,person1和person2,内部都有一个指向Person.prototype的指针,因此都返回true。

ECMAScript 5增加了一个新方法,叫Object.getPrototypeOf(),在所有支持的实现中,这个方法返回 [[Prototype]]的值。比如:

使用Object.getPrototypeOf()可以方便地取得一个对象的原型,而这在利用原型实现继承的情况下非常重要。支持这个方法的浏览器有IE9+、Firefox 3.5+、Safari 5+、Opera 12+和Chrome。

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。这种机制正是多个对象实例共享原型所保存的属性和方法的基本原理。

原型最初只包含constructor属性,而该属性也是共享的,因此可以通过对象实例访问。

虽然可以通过对象实例访问保存在原型中的值,但是,不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那么,我们就在实例中创建该属性,该属性将会屏蔽原型中的同名属性。比如:

当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。也就是说,添加这个属性只会阻止我们访问原型中的同名属性,但不会修改该属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用delete操作符则可以完全删除实例属性,从而重新恢复对原型属性的访问。