我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例信息,而是可以将这些信息直接添加到原型对象中。
function Person() {
}
Person.prototype.name = "Neo";
Person.prototype.age = 29;
Person.prototype.job = "Teacher";
Person.prototype.sayName = function() {
console.log(this.name);
}
var person1 = new Person();
person1.sayName();
var person2 = new Person();
person2.name = "Toby";
person2.age = 30;
person2.sayName();
console.log(person1.sayName === person2.sayName);
上面的代码的输出是:
- 理解原型对象
原型模式,有点类似于 C++ 的继承,每个对象都继承了来自原型对象中的值,这种继承是只读的,我们不能重写原型对象中的值,如果我们在实例中对原型对象中的同名属性赋值,其实是在实例中添加了一个新的属性,这个属性会屏蔽来自原型对象中的属性,而这点就类似于 C++ 中的覆盖。下面是实例:
function Person() {
}
Person.prototype.name = "Neo";
Person.prototype.age = 29;
Person.prototype.job = "Teacher";
Person.prototype.sayName = function() {
console.log(this.name);
}
var person1 = new Person();
person1.sayName(); // 来自原型的值
var person2 = new Person();
person2.name = "Toby";
person2.age = 30;
person2.sayName(); // 来自实例的值
person1.sayName(); // 来自原型的值
输出结果:
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。
我们已经知道,如果我们在实例中对原型对象中的同名属性赋值,其实是在实例中添加了一个新的属性,这个属性会屏蔽来自原型对象中的属性。即使将这个属性设置为 null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用 delete 操作符则可以完全删除实例属性,从而让我们可以重新访问原型中的属性,如下所示:
function Person() {
}
Person.prototype.name = "Neo";
Person.prototype.age = 29;
Person.prototype.job = "Teacher";
Person.prototype.sayName = function() {
console.log(this.name);
}
var person2 = new Person();
person2.name = "Toby";
person2.age = 30;
person2.sayName(); // 来自实例的值
delete person2.name;
person2.sayName(); // 来自原型的值
输出结果:
使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘记它是从 Object 继承来的)只在给定属性存在于对象实例中时,才会返回 true。下面是个实例:
function Person() {
}
Person.prototype.name = "Neo";
Person.prototype.age = 29;
Person.prototype.job = "Teacher";
Person.prototype.sayName = function() {
console.log(this.name);
}
var person1 = new Person();
person1.sayName(); // 来自原型的值
console.log("person1.hasOwnProperty(\"name\") :", person1.hasOwnProperty("name"));
var person2 = new Person();
person2.name = "Toby";
person2.age = 30;
console.log("person2.hasOwnProperty(\"name\") :", person2.hasOwnProperty("name"));
person2.sayName(); // 来自实例的值
delete person2.name;
person2.sayName(); // 来自原型的值
console.log("person2.hasOwnProperty(\"name\") :", person2.hasOwnProperty("name"));
实例的输出结果:
- 原型与 in 操作符
有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。在单独使用时,in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中,下面是使用实例:
function Person() {
}
Person.prototype.name = "Neo";
Person.prototype.age = 29;
Person.prototype.job = "Teacher";
Person.prototype.sayName = function() {
console.log(this.name);
}
var person1 = new Person();
person1.sayName(); // 来自原型的值
console.log("person1.hasOwnProperty(\"name\"): ", person1.hasOwnProperty("name"));
console.log("\"name\" in person1: ", "name" in person1);
var person2 = new Person();
person2.name = "Toby";
person2.age = 30;
console.log("person2.hasOwnProperty(\"name\"): ", person2.hasOwnProperty("name"));
console.log("\"name\" in person2: ", "name" in person2);
person2.sayName(); // 来自实例的值
delete person2.name;
person2.sayName(); // 来自原型的值
console.log("person2.hasOwnProperty(\"name\"): ", person2.hasOwnProperty("name"));
console.log("\"name\" in person2: ", "name" in person2);
console.log("\"gender\" in person2: ", "gender" in person2);
输出结果:
同时使用 hasOwnProperty 方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。
function hasPrototypeProperty(object, propertyString) {
return !object.hasOwnProperty(propertyString) && (propertyString in object);
}
下面是其使用实例:
function hasPrototypeProperty(object, propertyString) {
return !object.hasOwnProperty(propertyString) && (propertyString in object);
}
function Person() {
}
Person.prototype.name = "Neo";
Person.prototype.age = 29;
Person.prototype.job = "Teacher";
Person.prototype.sayName = function() {
console.log(this.name);
}
var person1 = new Person();
person1.sayName(); // 来自原型的值
console.log("person1.hasOwnProperty(\"name\"): ", person1.hasOwnProperty("name"));
console.log("\"name\" in person1: ", "name" in person1);
console.log("hasPrototypeProperty(person1, \"name\"): ", hasPrototypeProperty(person1, "name"));
var person2 = new Person();
person2.name = "Toby";
person2.age = 30;
console.log("person2.hasOwnProperty(\"name\"): ", person2.hasOwnProperty("name"));
console.log("\"name\" in person2: ", "name" in person2);
console.log("hasPrototypeProperty(person2, \"name\"): ", hasPrototypeProperty(person2, "name"));
person2.sayName(); // 来自实例的值
delete person2.name;
person2.sayName(); // 来自原型的值
console.log("person2.hasOwnProperty(\"name\"): ", person2.hasOwnProperty("name"));
console.log("\"name\" in person2: ", "name" in person2);
console.log("hasPrototypeProperty(person2, \"name\"): ", hasPrototypeProperty(person2, "name"));
其输出结果如下:
- 更简单的原型语法
function Person() {
}
Person.prototype = {
name: "Neo",
age: 29,
job: "Teacher",
sayName: function() {
console.log(this.name);
}
}
// 以下代码,确保通过 constructor 属性还能像之前的语法那样能够访问到适当的值
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
- 原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来 —— 即使是先创建了实例后修改原型也照样如此,下面是一个实例:
function Person() {
}
var friend = new Person();
Person.prototype.sayHi = function() {
console.log("hi");
}
friend.sayHi();
输出结果:
但是如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型 [[prototype]] 的指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。下面是一个实例:
function Person() {
}
var friend = new Person();
Person.prototype = {
name: "Neo",
age: 29,
job: "Teacher",
sayName: function() {
console.log(this.name);
}
}
// 以下代码,确保通过 constructor 属性还能像之前的语法那样能够访问到适当的值
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
friend.sayName();
这个例子会报错:
因为 friend 指向的原型中不包含以 sayName 命名的函数。重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。
- 原型对象的问题
原型模式也不是没有缺点。首先,他省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式的最大问题是由其共享的本性所导致的。
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,毕竟,通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了。请看下面这个例子:
function Person() {
}
Person.prototype = {
name: "Neo",
age: 29,
job: "Teacher",
friends: ["Toby", "Tina"],
sayName: function() {
console.log(this.name);
}
}
// 以下代码,确保通过 constructor 属性还能像之前的语法那样能够访问到适当的值
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
var person1 = new Person();
var person2 = new Person();
person2.friends.push("Tim");
console.log(person1.friends);
console.log(person2.friends);
console.log(person1.friends === person2.friends);
输出结果:
在此,假如我们的初衷就是所有对象共享一个 friends 数组的话,是没有问题的。但是现实中,这样的情况少之又少,因此,我们不应该单独使用原型模式,我们该怎么做呢?请关注下一节中介绍的内容。