本节目录
一、prototype(原型)属性示例
我们创建的每个函数都有一个 prototype
(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
如果按照字面意思来理解,那么 prototype
就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法:
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); //true
在此,与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,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()
。这是通过查找对象属性的过程来实现的。
2.1 Object.getPrototypeOf() 方法 ---- 获取对象的原型
使用 Object.getPrototypeOf()
可以方便地取得一个对象的原型:
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"
2.2 不能通过对象实例重写原型中的值
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。
如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。
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();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name); //"Greg"——来自实例
alert(person2.name); //"Nicholas"——来自原型
在这个例子中,person1
的 name
被一个新值给屏蔽了。但无论访问 person1.name
还是访问 person2.name
都能够正常地返回值,即分别是"Greg"
(来自对象实例)和"Nicholas"
(来自原型)。
当在 alert()
中访问 person1.name
时,需要读取它的值,因此就会在这个实例上搜索一个名为 name
的属性。这个属性确实存在,于是就返回它的值而不必再搜索原型了。当以同样的方式访问 person2. name
时,并没有在实例上发现该属性,因此就会继续搜索原型,结果在那里找到了 name
属性。
2.3 delete 操作符 ---- 删除实例属性
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。
即使将这个属性设置为 null
,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。
不过,使用 delete
操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性,如下所示。
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();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name); //"Greg"——来自实例
alert(person2.name); //"Nicholas"——来自原型
delete person1.name;
alert(person1.name); //"Nicholas"——来自原型
2.4 批量写原型方法
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
前面曾经介绍过,每创建一个函数,就会同时创建它的 prototype
对象,这个对象也会自动获得 constructor
属性。而我们在这里使用的语法,本质上完全重写了默认的 prototype
对象,因此 constructor
属性也就变成了新对象的 constructor
属性(指向 Object
构造函数),不再指向 Person
函数。此时,尽管 instanceof
操作符还能返回正确的结果,但通过 constructor
已经无法确定对象的类型了,如下所示。
var friend = new Person();
alert(friend instanceof Object); //true
alert(friend instanceof Person); //true
alert(friend.constructor == Person); //false
alert(friend.constructor == Object); //true
如果 constructor
的值真的很重要,可以像下面这样特意将它设置回适当的值。
function Person(){
}
Person.prototype = {
constructor : Person, // 加上这一行
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
这种方式的问题和解决方法:
以这种方式重设 constructor
属性会导致它的[[Enumerable]]
特性被设置为 true
。默认情况下,原生的 constructor
属性是不可枚举的,因此如果你使用兼容 ECMAScript 5
的 JavaScript
引擎,可以试一试 Object.defineProperty()
。
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
//重设构造函数,只适用于 ECMAScript 5 兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
三、检测属性
3.1 hasOwnProperty() 方法 ---- 检测属性是否存在于实例中
使用 hasOwnProperty()
方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从 Object
继承来的)只在给定属性存在于对象实例中时,才会返回 true
。来看下面这个例子。
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();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
person1.name = "Greg";
alert(person1.name); //"Greg"——来自实例
alert(person1.hasOwnProperty("name")); //true
alert(person2.name); //"Nicholas"——来自原型
alert(person2.hasOwnProperty("name")); //false
delete person1.name;
alert(person1.name); //"Nicholas"——来自原型
alert(person1.hasOwnProperty("name")); //false
通过使用 hasOwnProperty()
方法,什么时候访问的是实例属性,什么时候访问的是原型属性就一清二楚了。
下图展示了上面例子在不同情况下的实现与原型的关系(为了简单起见,图中省略了与 Person
构造函数的关系)。
3.2 in 操作符 ---- 此处检测属性是否存在(包括原型)
在单独使用时,in
操作符会在通过对象能够访问给定属性时返回 true
,无论该属性存在于实例中还是原型中:
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();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
person1.name = "Greg";
alert(person1.name); //"Greg" ——来自实例
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1); //true
alert(person2.name); //"Nicholas" ——来自原型
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2); //true
delete person1.name;
alert(person1.name); //"Nicholas" ——来自原型
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
同时使用 hasOwnProperty()
方法和 in
操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中,如下所示。
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person = new Person();
alert(hasPrototypeProperty(person, "name")); //true
person.name = "Greg";
alert(hasPrototypeProperty(person, "name")); //false
由于 in
操作符只要通过对象能够访问到属性就返回 true
,hasOwnProperty()
只在属性存在于实例中时才返回 true
,因此只要 in
操作符返回 true
而 hasOwnProperty()
返回 false
,就可以确定属性是原型中的属性。
在这里,name
属性先是存在于原型中,因此 hasPrototypeProperty()
返回 true
。当在实例中重写 name
属性后,该属性就存在于实例中了,因此 hasPrototypeProperty()
返回 false
。即使原型中仍然有 name
属性,但由于现在实例中也有了这个属性,因此原型中的 name
属性就用不到了。
四、枚举属性
4.1 for-in() 方法 ---- 这里可用于枚举属性
在使用 for-in
循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated
)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。
4.2 Object.keys() 方法 ---- 返回所有可枚举属性
这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。例如:
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var keys = Object.keys(Person.prototype);
alert(keys); //"name,age,job,sayName"
var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
alert(p1keys); //"name,age"
4.3 Object.getOwnPropertyNames() 方法 ---- 返回(包括不可枚举)属性
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); //"constructor,name,age,job,sayName"
五、原型的动态性
例一:
var friend = new Person();
Person.prototype.sayHi = function(){
alert("hi");
};
friend.sayHi(); //"hi"(没有问题!)
这个例子中,即使 person
实例是在添加新方法之前创建的,但它仍然可以访问这个新方法。其原因可以归结为实例与原型之间的松散连接关系。
当我们调用 person.sayHi()
时,首先会在实例中搜索名为 sayHi
的属性,在没找到的情况下,会继续搜索原型。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的 sayHi
属性并返回保存在那里的函数。
例二(重写原型的情况):
function Person(){
}
var friend = new Person();
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
friend.sayName(); //error
在这个例子中,我们先创建了 Person
的一个实例,然后又重写了其原型对象。然后在调用 friend.sayName()
时发生了错误,因为 friend
指向的原型中不包含以该名字命名的属性。
重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。
六、原生对象的原型
所有原生引用类型(Object
、Array
、String
,等等)都在其构造函数的原型上定义了方法。
例如,在 Array.prototype
中可以找到 sort()
方法,而在 String.prototype
中可以找到 substring()
方法。
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。不过不建议这样操作。
七、原型对象的缺点
它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。然而,对于包含引用类型值的属性来说,问题就比较大了。来看下面的例子:
function Person(){
}
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
friends : ["Shelby", "Court"],
sayName : function () {
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //true
在此,Person.prototype
对象有一个名为 friends
的属性,该属性包含一个字符串数组。
然后,创建了 Person
的两个实例。
接着,修改了 person1.friends
引用的数组,向数组中添加了一个字符串。
由于 friends
数组存在于 Person.prototype
而非 person1
中,所以刚刚提到的修改也会通过 person2.friends
(与 person1.friends
指向同一个数组)反映出来。
`