JavaScript高级程序设计-原型

# 理解原型
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个`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()的枚举顺序是确定的。先以升序枚举数值键,然后以插入顺序枚举字符串和符号键。在对象字面量中定义的键以它们逗号分隔的顺序插入。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值