原型模式创建对象
每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象(prototype)就是通过调用构造函数创建的对象的原型。使用原型对象的好处
是,在它上面定义的属性和方法可以被对象实例共享
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
person1.sayName(); // "Nicholas"
let person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true
这里,构造函数上并没有定义任何属性和方法,但是,我们new出来的实例上却能够访问到相应的属性和方法。与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。因此 person1 和 person2 访问的都是相同的属性和相同的 sayName()函数。要理解这个行为,就必须理解ECMAScript中原型的本质
理解原型
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向
原型对象)。默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构
造函数。对前面的例子而言,Person.prototype.constructor 指向 Person。
每次调用构造函数创建一个新实例,这个实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象,如下:
实例内部[[Prototype]]指针指向构造函数的的原型对象,构造函数的原型对象的constructor指向构造函数本身。通过如下代码来理解:
//实例通过__proto__链接到原型对象,它实际上指向隐藏特性[[Prototype]]
console.log(person1.__proto__ === Person.prototype)//true
console.log(Person.prototype.constructor === Person)//true
//所以 实例是通过原型对象与构造函数间接关联
conosle.log(person1.__proto__.constructor === Person); // true
通过下图可以加深理解构造函数 原型对象 和实例之间的关系:
ECMAScript 的 Object 类型有一个方法叫 Object.getPrototypeOf(),返回参数的内部特性
[[Prototype]]的值。使用 Object.getPrototypeOf()可以方便地取得一个对象的原型 代码如下:
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name);//Nicholas
Object 类型还有一个 setPrototypeOf()方法,可以向实例的私有特性[[Prototype]]写入一
个新值。这样就可以重写一个对象的原型继承关系。如下:
let biped = {
numLegs: 2
};
let person = {
name: 'Matt'
};
Object.setPrototypeOf(person, biped);
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
但 Object.setPrototypeOf()可能会严重影响代码性能。所以我们要避免使用setPrototypeOf来修改对象的原型。改用通过 Object.create()来创建一个新对象,同时为其指定原型:
let biped = {
numLegs: 2
};
let person = Object.create(biped);
person.name = 'Matt';
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
重写够造函数的原型对象
前面通过原型模式创建对象时,会重复写很多次prototype。为了避免代码冗余,可以将定义在protoype上的属性和方法集中写到一个对象字面量上来重写prototype,如下:
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
这样 是简化了原型上定义多个属性或方法的问题 。但也带来了一个问题,即Person.prototype 的 constructor 属性就不指向 Person,而是指向了 Object 构造函数。虽然 instanceof 操作符还能可靠地返回 值,但我们不能再依靠 constructor 属性来识别类型了
//constructor不能正确指向Person ,就不能以constructor来判断类型了
console.log(Person.prototype.constructor) //Object() { [native code] }
//instanceof 能正确的类型判断
console.log(friend instanceof Person);//true
可以像如下方式来修复这个问题,即显示的把constructor指向Person构造函数
function Person() {
}
Person.prototype = {
constructor: Person, //恢复 constructor 属性 此时可枚举
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
但要注意,以这种方式恢复 constructor 属性会创建一个[[Enumerable]]为 true 的属性。更进一步的,如果浏览器能支持Object.defineProperty() 则我们可以使用Object.defineProperty() 来定义不可枚举的constructor属性
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
// 恢复 constructor 属性 此时不可枚举
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
原型的动态性
因为从原型上搜索值的过程是动态的,所以即使实例定义在申明原型属性之前,也是可以通过实例去访问到后面添加或修改的原型属性,如下:
let friend = new Person();
Person.prototype.sayHi = function() {
console.log("hi");
};
Person.prototype.age = 30
friend.sayHi(); // "hi"
console.log(friend.age)//30
之所以这样,是因为原型对象和实例之间松散的联系(简单的指针,而不是保存的副本)。当在对象上调用方法时,会先在实例中搜索对应的方法,找不到再去原型对象上查找对应的方法。
虽然 可以随时给原型添加属性和方法,并能够立即反映在所有对象实例上。但是如果在申明实例后重写了整个原型对象,就会达不到用户所想要的目的。如下:
function Person() {}
let friend = new Person();
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
friend.sayName(); // friend.sayName is not a function
这是因为申明实例时 ,实例的[[Prototype]]指针已经指向了 Person.prototype了。当后续把Person.prototype整个对象修改了,指针也不会改变了。所以friend实例指向的原型对象仍然是最初的原型。记住一点,实例有一个指针指向原型对象,没有指针指向构造函数。 重写构造函数上的原型之后再创建的实例才会引用新的原型。而在此之前创建的实例仍然会引用最初的原型。
原型模式的问题
原型模式也不是没有问题。首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默
认都取得相同的属性值。虽然这会带来不便,但还不是原型的最大问题。原型的最主要问题源自它的共
享特性。如下:
function Person() {}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
friends: ["Shelby", "Court"],
sayName() {
console.log(this.name);
}
};
let person1 = new Person();
let person2 = new Person();
person1.friends.push("Van");
console.log(person1.friends); // "Shelby,Court,Van"
console.log(person2.friends); // "Shelby,Court,Van"
console.log(person1.friends === person2.friends); // true
person1.friends 通过 push 方法向数组中添加了一个字符串。由于这个friends 属性存在于 Person.prototype 而非 person1 上,新加的这个字符串也会在(指向同一个数组的)person2.friends 上反映出来。如果这是有意在多个实例间共享数组,那没什么问题。但一般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。