1. 对象创建
1.5 原型模式
前文我们提到过,每一个函数都有一个prototype属性,它是一个指针,指向一个对象,找个对象保存了所有函数实例可以共享的属性和方法。因此,定义自定义引用类型时,可以直接将属性和方法添加到原型对象中。请看下面的例子:
function Person(){
}
Person.prototype.name = "Ivan";
Person.prototype.age = 22;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
}
var person1 = new Person();
person1.sayName(); //"Ivan"
var person2 = new Person();
person2.sayName(); //"Ivan"
alert(person1.sayName == person2.sayName); //true
在上面的例子中,我们将所有的属性和方法都直接添加到Person的prototype中,构造函数变成了空函数。然而我们依然能够通过构造函数来创建新对象,并且通过Person实例来调用方法和访问属性。但与构造函数模式不同的是,上面的这些属性和方法由所有的实例共享,因此这也是一个问题(事实上我们希望方法在实例间共享,而属性则独属于各个实例,后文会解决这个问题)。
1.5.1 理解原型对象
无论在什么时候,只要创建了一个函数,JavaScript引擎就会根据一组特定的规则为该函数创建一个prototype对象,并将函数的prototype属性指向该对象。另外,每个prototype对象也包含一个constructor属性,指向持有它的函数。用前面的例子来说:Person.prototype.constructor = Person
。
根据构造函数创建出一个实例后,实例内部也包含一个指针,指向构造函数的原型对象。ECMA-262第5版管这个指针叫[[Prototype]]。虽然在JavaScript中没有明确的方式来访问这个指针,但是在Firefox,Safari和Chrome中,都支持一个proto属性来访问这个[[Prototype]]。真正需要明确的一点是,这个指针负责连接实例与构造函数的原型对象,而不是连接实例与构造函数。下面展示了Person构造函数及其实例与原型对象的关系:
从图中我们可以看到,Person对象的两个实例person1和person2,都包含一个[[Prototype]]属性,指向了Person.prototype对象;换句话说,这两个实例与Person构造函数其实并没有直接的关系。此外,需要格外注意的是,虽然这两个实例都不包含任何属性和方法,却能够成功调用person1.sayName()
等Person.prototype定义的方法,这是通过查找对先属性的过程来实现的。
每当代码读取对象的某个属性时,都会经历一次搜索过程,具体搜索过程如下:
(1) 搜索首先从对象本身开始,如果在实例中找到了具有给定名称的属性,则直接返回,否则进行下一步。
(2)搜索[[Prototype]]属性指向的原型对象,如果在原型对象中找到了具有给定名称的属性,则返回。
也就是说,在执行person1.sayName()
时,进行了两次搜索。首先查找person1对象,未找到sayName属性;接着查找person1.[[Prototype]]也就是Person.prototype对象,成功找到sayName方法,因此调用成功。
虽然在所有实现中都无法访问到[[Prototype]],但是可以通过isPrototypeOf()方法来确定对象间是否存在原型关系,例如:
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
ECMAScript5增加了一个新方法,叫Object.getPrototypeOf()
,在所有支持的视实现中,这个方法返回[[Prototype]]的值:
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Ivan"
原型对象最初只包含constructor属性,而该属性也是共享的,因此也可以通过实例访问该属性。
虽然可以通过实例来访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在对象中添加了一个原型中的同名属性,这个属性会与原型中属性相互独立,通过person实例修改这个属性并不会影响到原型中的同名属性。下面是一个例子:
function Person(){
}
Person.prototype.name = "Ivan";
Person.prototype.age = 22;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
}
var person1 = new Person();
var person2 = new Person();
person1.name = "Roy"
alert(person1.name); //"Roy"
alert(person2.name); //"Ivan"
上面的例子表明,当我们重写person1.name时,实际上是在person1实例对象中添加了一个名为name的属性,并赋值为"Roy"。此后在读取person1.name时,首先在person1实例中就找到了该属性,因此直接返回"Roy";而读取person2.name时,person2实例对象中不存在name属性,因此继续搜索其原型对象,找到Person.prototype.name后,返回其值"Ivan"。
使用person1.hasOwnProperty("name")方法能够判断name属性是否属于对象实例。hasOwnProperty()方法继承自Object类型,如果给定属性存在于对象实例中(而不是原型中),那么返回true。
当对象能够访问某个属性时(无论本身具有该属性还是通过原型能够访问该属性),使用propertyName in object
会返回true。这里的in操作符是判断对象实例是否包含某个属性的关键。通过hasOwnProperty()方法和in操作符,就能够判断某个属性是否独属于原型对象:
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
使用Object.keys()能够返回一个对象中所有可枚举的实例属性(不搜索原型),这个方法接收一个对象作为参数,返回一个字符串数组。
如果你想要得到所有实例属性,无论它是否可枚举,可以使用Object.getOwnPropertyNames() 方法。
constructor属性是不可枚举的,因此Object.keys(Person.prototype)返回:name, age, job, sayName。
1.5.2 更简单的原型写法
前面的例子中每定义一个属性或方法都得敲一遍Person.prototype。为了减少不必要的输入,也为了从视觉上更好的封装原型的功能,更常见的做法是使用一个包含了所有属性和方法的字面量来重写整个原型对象:
function Person(){}
Person.prototype = {
name: "Ivan",
age: 22,
job: "Software Engineer",
sayName: function(){
alert(this.name);
}
}
上面的例子将Person.prototype设置为了一个以对象字面量形式创建的新对象。因此带来了一个问题:constructor属性不再指向Person了。前面提到,每创建一个函数,就会自动创建一个它的prototype对象,这个对象自动将constructor属性指向源函数。前面的定义诸如Person.prototype.name = "Ivan;"
都只是在自动生成的prototype对象中定义属性和方法,而在这个例子中,我们手动将Person.prototype指向了一个新对象(以对象字面量形式创建),这个新对象constructor属性默认指向Object。尽管如此,instancof操作符依旧能返回正确的结果。
如果你觉得constructor属性真的很重要,可以再定义时显示的设置其值:
function Person(){}
Person.prototype = {
constructor: Person,
name: "Ivan",
age: 22,
job: "Software Engineer",
sayName: function(){
alert(this.name);
}
}
注意,以这种方式重设 constructor 属性会导致它的[[Enumerable]]特性被设置为 true。默认 情况下,原生的 constructor 属性是不可枚举的,因此如果你使用兼容 ECMAScript 5的 JavaScript引 擎,可以试一试 Object.defineProperty()。
1.5.3 组合使用构造函数模式和原型模式
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒 也说得过去,毕竟(如前面的例子所示),通过在实例上添加一个同名属性,可以隐藏原型中的对应属 性。然而,对于包含引用类型值的属性来说,问题就比较突出了。来看下面的例子:
function Person(){}
Person.prototype = {
constructor: Person,
name: "Ivan",
age: 22,
job: "Software Engineer",
friends: ["Nancy", "Bob"],
sayName: function(){
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push("John");
alert(person1.friends); //"Nancy, Bob, John"
alert(person2.friends); //"Nancy, Bob, John"
我们可以看到,对于一个数组的引用类型,我们修改person1对象的friends属性,对于person2对象来说也是可见的,因为本质上person1对象和person2对象都指向原型对象上的friends数组。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。
因此,创建自定义类型的常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实 例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本, 但同时又共享着对方法的引用,大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参 数;可谓是集两种模式之长。下面的代码重写了前面的例子:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
这种构造函数与原型混成的模式,是目前在 ECMAScript中使用广泛、认同度高的一种创建自 定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。
1.5.4 动态原型模式
对于拥有其他OO语言开发经验的开发人员来说,看到独立的构造函数和原型很可能会感到困惑。动态原型模式正是致力于解决这一问题的方案。它将所有信息封装在构造函数中,并在构造函数中初始化原型。下面是一个例子:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
var person= new Person("Ivan", 22, "Software Engineer");
person.sayName(); //"Ivan"
网友评论