我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。 字面意思来理解的话, 所谓prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有的对象实例共享它所包含的属性和方法。换句话说, 不必再构造函数中定义对象实力的信息,而是可以将这些信息直接添加到原型对象中,如下面的例子所示:
function Person() {
}
Person.prototype.name = '张三';
Person.prototype.age =18 ;
Person.prototype.job = '老师';
Person.prototype.sayName = function () {
console.log(this.name)
};
var person1 = new Person();
person1.sayName(); //返回张三
var person2 = new Person();
person2.sayName(); // 返回张三;
console.log(person1.sayName == person2.sayName); //返回true
在此, 我们将sayName的方法和属性直接添加到了Person函数的prototype属性中,构造函数变成了空函数。即使如此, 我们依然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。
但与构造函数不同的是,新对象的这些属性和方法是所有的实例共享的。换句话说, person1和person2访问的都是同一组属性的同一个sayName()函数。要理解原型模式的工作原理, 必须要先理解ECMAScript中原型对象的性质。
理解原型对象
无论什么时候, 只要创建了一个新函数,就根据一组特定的规则为该函数创建一个prototype的属性, 这个属性指向函数的原型对象。在默认的情况下,所有的原型对象都会自动获得一个constructor(构造函数)的属性,这个属性是一个指向prototype属性函数所在函数的指针。就拿前面例子来说, Person.prototype.constructor指向的是Person。通过构造函数,我们还可以继续为原型对象添加其他的属性和方法。
创建了自定义函数后,其原型对象默认只会取得constructor属性;至于其他方法, 都是从Object继承而来的。当调用构造函数创建一个新实例后,该势力的内部包含一个指针(内部属性),指向构造函数的原型对象。
前面使用的Person构造函数和Person.prototype为例,下图展示了各个对象之间的关系:
上图展示了Person构造函数, Person的原型属性以及Person现有的两个势力之间的关系。
在此,Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。原型对象中除了包含constructor属性之外,还包括其他添加进来的其他属性。Person的每个实例——person1和person2都包含一个内部属性,该属性仅仅指向了Person.prototype;换句话说, 它们与构造函数没有直接的关系。此外,要格外注意的是,虽然这两个实例都不包含属性和方法,但我们却能够调用person1.sayName()。这是通过查找对象属性的过程来实现的。
虽然在所有的实现中都无法访问到[[prototype]],但是可以通过调用isPrototypeOf()的方法来确定对象之间是否存在这种关系。从本质上讲,如果[[prototype]]指向调用isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回true, 如下所示:
console.log(Person.prototype.isPrototypeOf(person1)) // 返回true;
console.log(Person.prototype.isPrototypeOf(person2)) // 返回true;
这里 我们使用了原型对象的isPrototypeOf()的方法测试了person1和person2,因为他们内部都有一个指向Person.prototype的指针, 因此都返回了true。
ECMAScript5增加了一个新方法, 叫Object.getPrototypeOf(),在所有支持的实现中,这个方法都返回[[prototype]]的值。例如:
console.log(Object.getPrototypeOf(person1) == Person.prototype); //返回true
console.log(Object.getPrototypeOf(person1).name); // 返回张三
这里的第一行代码只是确定了Object.getPrototypeOf()返回的对象实际就是这个对象的原型。第二行代码取得了原型对象中的name属性的值,也就是‘张三’,使用Object.getPrototypeOf()可以方便地取到一个对象的原型,而这在利用原型实现继承的情况下是非常重要的。支持这个方法的浏览器有:IE9+, Firefox3.5+, Safari5+, Opera12+和 Chrome。
每当代码读取某个对象的属性时, 都会执行一次搜索,目标是具有给定名字的属性。首先从对象实例的本身开始搜索。 如果在实例中找到了据有关给定名字的属性,则返回该属性的值,如果没有找到, 则继续搜索指针指向的原型对象在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性, 则返回改属性的值。也就是说,当我们调用person1.sayName()时,会先后执行两次搜索。首先, 解析器会问:“实例person1有sayName属性吗?”答:“没有。”然后, 它继续搜索,再问:“person1的原型有sayNameshuxingma?”答:“有。”于是,他就读取那个保存在原型对象中的函数。当我们调用person2.sayName()时, 会重现相同的搜索过程,得到相同的结果。而这正是多个对象实例共享原型所保存的属性和方法的基本原理。