在同一类的对象之间共享属性
原型模式用于在同一类型的对象之间共享属性。原型是JavaScript原生的一种对象,并且可以通过对象的原型链直接访问。
在应用中,我们通常会创建同一种类型的很多对性。通常我们会通过ES6的类定义来达成这一目的。
比如希望创建很多狗类对象,当然并不是创建了很多狗类:而是通过狗的类型定义,为不同的狗赋予不同的名字,并且,他们具有相同的行为,也就是犬吠。
class Dog {
constructor(name) {
this.name = name;
}
bark() {
return `Woof!`;
}
}
const dog1 = new Dog("Daisy");
const dog2 = new Dog("Max");
const dog3 = new Dog("Spot");
请注意构造函数包含一个name属性,以及类定义中包含一个bark的属性。在ES6的类定义中,所有在类定义中声明的属性,比如在本例中的bark,都会被自动添加到prototype中。
如果要近距离观察prototype,可以通过类的构造函数中的prototype属性对其进行访问,或者通过任何实例的__proto__属性进行访问。
console.log(Dog.prototype);
// constructor: ƒ Dog(name, breed) bark: ƒ bark()
console.log(dog1.__proto__);
// constructor: ƒ Dog(name, breed) bark: ƒ bark()
构造函数派生的任何实例的__proto__属性,都直接引用了构造函数的原型。当访问对象中不直接存在的属性时,JavaScript都会向上追溯整个原型链,看看是否在原型链的上游存在这个属性。
面向共有属性的对象编程时,原型模式是非常强大的。相较于为每一个对象都重复创建相同的属性,在JavaScript中可以使用原型模式,因为原型模式允许所有派生自同一个类或者父类的对象都可以访问祖先们的属性。
既然所有实例都可以访问原型,那么也就意味着甚至可以在实例被创建之后,仍然可以向原型添加新的属性。
比如在狗狗类声明中,突然发现狗狗不应该只会叫,还应该会玩!那么我们可以随时为狗狗类的类声明添加play的属性。
class Dog {
constructor(name) {
this.name = name;
}
bark() {
return `Woof!`;
}
}
const dog1 = new Dog("Daisy");
const dog2 = new Dog("Max");
const dog3 = new Dog("Spot");
Dog.prototype.play = () => console.log("Playing now!");
dog1.play();
原型链这个术语从字面上来看,既然是链条就意味着可以不仅有一层。确实如此!截止目前位置,我们都只讨论了在实例对象中的直接可见的__proto__属性。然而__proto__引用的原型本身也有一个__proto__对象。
为了验证这点,可以创建另一个类型的狗狗类声明:super dog。这类型的狗应该能够从普通的Dog类声明中继承其所有属性,与此同时它还能够飞。为了达成此目标,我们可以创建一个SuperDog类,扩展自Dog类,并且新添加一个fly方法。
class SuperDog extends Dog {
constructor(name) {
super(name);
}
fly() {
return "Flying!";
}
}
之后通过实例化SuperDog类,得到狗狗对象Daisy,这只新狗狗可以叫,并且能够飞翔。
class Dog {
constructor(name) {
this.name = name;
}
bark() {
console.log("Woof!");
}
}
class SuperDog extends Dog {
constructor(name) {
super(name);
}
fly() {
console.log(`Flying!`);
}
}
const dog1 = new SuperDog("Daisy");
dog1.bark();
dog1.fly();
因为扩展了Dog类,所以实例可以调用bark方法。深层次的原因在于SuperDog的原型中的__proto__的引用正是指向了Dog.prototype对象。
关于所谓原型链的真相愈发明晰:当我们访问一个对象中并不直接存在于对象本身的属性时,JavaScript会递归地沿着原型链,通过访问原型链路径链路上所有的__proto__属性向上层的寻找,直到找到这一属性。
Object.create
Object.create方法允许通过它创建一个新的对象,通过这个方法可以明确地传递原型作为参数。
const dog = {
bark() {
return `Woof!`;
}
};
const pet1 = Object.create(dog);
虽然pet1对象本身并没有任何属性,但是由于我们向create方法传递了dog对象作为参数,因此pet1对象也获取了原型链上层对象的bark属性的访问能力。
const dog = {
bark() {
console.log(`Woof!`);
}
};
const pet1 = Object.create(dog);
pet1.bark(); // Woof!
console.log("Direct properties on pet1: ", Object.keys(pet1));
console.log("Properties on pet1's prototype: ", Object.keys(pet1.__proto__));
Object.create通过明确新创建的实例来自于哪个对象原型,可以明确新创建的对象能够继承哪个对象的属性。所以新创建的对象可以访问原型链上游的属性。
原型模式可以让使用者轻松的继承和访问其他对象的属性。由于原型链允许访问那些并不在类本身定义过的属性,所以也就避免了重复声明方法和属性,最终在运行时节约了内存消耗。