# 理解原型
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个`prototype`属性(指向原型对象)。默认情况下,所有原型对象自动获得一个名为`constructor`的属性,指回与之关联的构造函数。
在定义构造函数时,原型对象默认只会获得`constructor`属性,其他的所有方法都继承自`Object`。每次调用构造函数创建一个新实例,这个实例的内部`[[Prototype]]`指针就会被赋值为构造函数的原型对象。脚本中没有访问这个`[[Prototype]]`特性的标准方式,但Firefox/Safari/Chrome会在每个对象暴露`__proto__`属性,通过这个属性可以访问对象的原型。
实例与构造函数原型有直接的联系,但实例与构造函数之间没有
这种关系不好可视化,但可以通过下面的代码来理解原型的行为
```javascript
function Person() {}
// 声明之后,构造函数就有了一个与之关联的原型对象
console.log(typeof Person.prototype);
console.log(Person.prototype)
// 如前所述,构造函数有一个prototype属性
// 引用其原型对象,而这个原型对象有一个constructor属性,引用这个构造函数
// 换句话说,两者循环引用
console.log(Person.prototype.constructor === Person) // true
// 正常的原型链都会终止于Object的原型对象
// Object原型的原型是null
console.log(Person.prototype.__proto__ === Object.prototype) // true
console.log(Person.prototype.__proto__.constructor === Object) // true
console.log(Person.prototype.__proto__.__proto__ === null) // true
let person1 = new Person()
let person2 = new Person()
// 构造函数、原型对象和实例时3个完全不同的对象
console.log(person1 !== Person) // true
console.log(person1 !== Person.prototype) // true
console.log(Person.prototype !== Person) // true
// 实例通过__proto__链接到原型对象,它实际指向隐藏特性[[Prototype]]
// 构造函数通过prototype属性链接到原型对象
// 实例与构造函数没有直接联系,与原型对象有直接联系
console.log(person1.__proto__ === Person.prototype) // true
console.log(person1.__proto__.constructor === Person) // true
// 同一个构造函数创建的两个实例,共享同一个原型对象
console.log(person1.__proto__ === person2.__proto__) // true
// instanceof 检查实例的原型链中是否包含指定构造函数的原型
console.log(person1 instanceof Person) // true
console.log(person1 instanceof Object) // true
console.log(Person.prototype instanceof Object) // true
```
上面的图片展示了Person构造函数、Person的原型对象和Person现有两个实例之间的关系。
注意:Person.prototype指向原型对象,而Person.prototype.constructor指回Person构造函数。
原型对象包含constructor属性和其他后来添加的属性。
Person的两个实例person1和person2都只有一个内部属性指回Person.prototype,而且两者都与构造函数没有直接联系。
另外要注意,虽然这两个实例都没有属性和方法,但person1.sayName()都可以正常调用,这是由于对象属性查找机制的原因。
虽然不是所有实现都对外暴漏了[[Prototype]],但可以使用isPrototypeOf()方法确定两个对象之间的这种关系,本质上,isPrototypeOf()会在传入参数的[[Prototype]]指向调用它的对象时返回true,如下所示
```
console.log(Person.prototype.isPrototypeOf(person1) // true
console.log(Person.prototype.isPrototypeOf(person2)) // true
```
这里通过原型对象调用isPrototypeOf()方法检查了person1和person2.因为这两个例子内部都有链接指向Person.prototype,所以结果都返回true。
ESMAScript的Object类型有一个方法叫Object.getPrototypeOf(),返回参数的内部特性[[Prototype]]的值。例如:
```
console.log(Object.getPrototypeOf(person1) === Peron.prototype // true
console.log(Object.getPrototypeOf(person1).name // '66'
```
第一行代码简单确认了Object.getPrototypeOf()返回的对象就是传入对象的原型对象。第二行代码则取得了原型对象上name属性的值,即'66'。使用Object.getPrototypeOf()可以方便地取得一个对象的原型,而这在通过原型实现继承时显得尤为重要。
可以通过Object.create()来创建一个新对象,同时为其指定原型:
```
let biped = {
numLegs: 2
}
let person = Object.create(biped)
person.name = '66'
console.log(person.name) // '66'
console.log(person.numLegs) // 2
console.log(Object.getPrototypeOf(person) === biped) // true
```
# 原型层级
在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后再原型对象上找到属性后,再返回对应的值。因此,在调用`person1.sayName()`时,会发生两部搜索。首先,JavaScript引擎会问:“person1实例有sayName属性吗?” 答案时没有。然后,继续搜索并问:“person1的实例有sayName属性吗?”答案时有。于是就返回了保存在原型上的这个函数。在调用person2.sayName()时同样。这就是原型用于再多个对象实例间共享属性和方法的原理。
constructor属性只存在于原型对象,因此通过实例对象也可以访问到
虽然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值。如果再实例上添加了一个与原型对象中同名的属性,那就会在实例上创建这个属性,这个属性会遮住原型对象上的属性。下面看一个例子:
```javascript
function Person() {}
Person.prototype.name = '66'
Person.prototype.age = 29
Person.prototype.color = 'blue'
Person.sayName = function() {
console.log(this.name)
}
let person1 = new Person()
let person2 = new Person()
person1.name = '51'
console.log(person1.name) // '51' 来自实例
console.log(person2.name) // '66' 来自原型
```
在这例子中,person1的name属性遮蔽了原型对象上的同名属性。只要给对象实例添加一个属性,这个属性就会`遮蔽(shadow)`原型对象上的同名属性,也就是虽然不会修改它,但会屏蔽对它的访问。即使在实例上把这个属性设置为null,也不会恢复它和原型的联系。不过,使用`delete`操作符可以完全删除实例上的这个属性,从而让标识符解析过程能继续搜索原型对象。
`hasOwnProperty()`方法用来确定某个属性实在实例上还是在原型对象上。这个方法时继承自Object的,会在属性存在于调用它的对象实例上时返回true,入下面的例子所示:
```javascript
console.log(person1.hasOwnProperty('name')) // true
console.log(person2.hasOwnProperty('name')) // false
```
# 原型和in操作符
有两种方式使用in操作符:单独使用和在for-in循环中使用。在单独使用时,in操作符会在可以通过对象访问指定属性时返回true,无论该属性是在实例上还是在原型上。来看下面的例子:
```javascript
function Person() {}
Person.prototype.name = '66'
Person.prototype.age = 2
Person.prototype.color = 'blue'
Person.sayName = function () {
console.log(this.name)
}
let person1 = new Person()
let person2 = new Person()
console.log(person1.hasOwnProperty('name')) // false
console.log('name' in person2) // true
person1.name = '666'
console.log(person1.name) // '666' 来自实例
console.log(person1.hasOwnProperty('name')) // trye
console.log('name' in person1) // true
console.log(person2.name) // "66",来自原型
console.log(person2.hasOwnProperty('name')) // false
console.log('name' in person2) // true
delete person1.name
console.log(person1.name) // "66",来自原型
console.log(person1.hasOwnProperty('name')) // false
console.log('name' in person1) // true
```
在这个例子中,name随时可以通过实例或原型访问到。因此调用`"name" in person1`时始终返回true,无论这个属性是否在实例上。如果要确定某个属性是否存在于原型上,则可以像下面这样同时使用hasOwnProperty()和in操作符:
```
function hasPrototypeProperty(object,name) {
return !object.hasOwnProperty(name) && (name in object)
}
```
只要通过对象可以访问,in操作符就返回true,而hasOwnProperty()只有属性存在于实例上时才返回true。因此,只要in操作符返回true且hasOwnProperty()返回false,就说明在原型上。
在for-in循环中使用in操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。遮蔽原型中不可枚举([[Enumerable]]特性被设置为false)属性的实例属性也会在for-in循环中返回,因为在默认情况下开发者定义的属性都是可枚举的。
要获得对象上所有可枚举的实例属性,可以使用Object.keys()方法。这个方法接受一个对象作为参数,返回包含该对象所有可枚举属性名称的字符串数组。
在ESMAScript6新增符号类型之后,相应地出现了增加一个Object.getOwnPropertyName()的兄弟方法的需求,因此以符号为键的属性没有名称的概念。因此,Object.getOwnPropertySymbols()方法就出现了,这个方法与Object.getOwnPropertyNames()类型,只是针对符号而已。
# 属性枚举顺序
for-in循环、Object.keys()、Object.getOwnPropertyName()、Object.getOwnPropertySymbols()以及Object.assign()在属性枚举顺序方面有很大区别。
for-in循环和Object.keys()的枚举顺序时不确定的,取决于JavaScript引擎,可能因浏览器而异。
Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和Object.assign()的枚举顺序是确定的。先以升序枚举数值键,然后以插入顺序枚举字符串和符号键。在对象字面量中定义的键以它们逗号分隔的顺序插入。