原型模式创建对象

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象(prototype)就是通过调用构造函数创建的对象的原型。使用原型对象的好处
是,在它上面定义的属性和方法可以被对象实例共享

  1. function Person() {}
  2. Person.prototype.name = "Nicholas";
  3. Person.prototype.age = 29;
  4. Person.prototype.job = "Software Engineer";
  5. Person.prototype.sayName = function() {
  6. console.log(this.name);
  7. };
  8. let person1 = new Person();
  9. person1.sayName(); // "Nicholas"
  10. let person2 = new Person();
  11. person2.sayName(); // "Nicholas"
  12. console.log(person1.sayName == person2.sayName); // true

这里,构造函数上并没有定义任何属性和方法,但是,我们new出来的实例上却能够访问到相应的属性和方法。与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。因此 person1 和 person2 访问的都是相同的属性和相同的 sayName()函数。要理解这个行为,就必须理解ECMAScript中原型的本质

理解原型

无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向
原型对象)。默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构
造函数。对前面的例子而言,Person.prototype.constructor 指向 Person。
每次调用构造函数创建一个新实例,这个实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象,如下:
1655107355(1).jpg
实例内部[[Prototype]]指针指向构造函数的的原型对象,构造函数的原型对象的constructor指向构造函数本身。通过如下代码来理解:

  1. //实例通过__proto__链接到原型对象,它实际上指向隐藏特性[[Prototype]]
  2. console.log(person1.__proto__ === Person.prototype)//true
  3. console.log(Person.prototype.constructor === Person)//true
  4. //所以 实例是通过原型对象与构造函数间接关联
  5. conosle.log(person1.__proto__.constructor === Person); // true

通过下图可以加深理解构造函数 原型对象 和实例之间的关系:
1655111833(1).jpg
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 上反映出来。如果这是有意在多个实例间共享数组,那没什么问题。但一般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。