深入理解js中的原型链(二)

177 阅读5分钟

文章来自于微信公众号coderwhy

二.原型链

1.1. 深入理解原型链

先来回顾一下构造函数、原型和实例的关系:
每个构造函数都有一个原型对象, 通过prototype指针指向该原型对象.
原型对象都包含一个指向构造函数的指针, 通过constructor指针, 指向构造函数
而实例都包含一个指向原型对象的内部指针, 该内部指针我们通常使用__proto__来描述.
思考如下情况:
我们知道, 可以通过Person.prototype = {}的方式来重写原型对象.
假如, 我们后面赋值的不是一个{}, 而是另外一个类型的实例, 结果会是怎么样呢?
显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。
假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
有些抽象, 我们通过代码来理解:

// 创建Person构造函数
function Person() {
}

// 设置Animal的原型
Person.prototype = {
}

我们将代码修改成原型链的形式:

// 1.创建Animal的构造函数
function Animal() {
    this.animalProperty = "Animal"
}

// 2.给Animal的原型中添加一个方法
Animal.prototype.animalFunction = function () {
    alert(this.animalProperty)
}

// 3.创建Person的构造函数
function Person() {
    this.personProperty = "Person"
}

// 4.给Person的原型对象重新赋值
Person.prototype = new Animal()

// 5.给Person添加属于自己的方法
Person.prototype.personFunction = function () {
    alert(this.personProperty)
}

// 6.创建Person的实例
var person = new Person()
person.animalFunction()
person.personFunction()

代码解析:
代码有一些复杂, 但是如果你希望学习好原型链, 必须耐心去看一看上面的代码, 你会发现其实都是我们学习过的.
重点我们来看第4步代码: 给Person.prototype赋值了一个Animal的实例. 也就是Person的原型变成了Animal的实例.
Animal实例本身有一个__proto__可以指向Animal的原型.
那么, 我们来思考一个问题: 如果现在搜索一个属性或者方法, 这个时候会按照什么顺序搜索呢?

  • 第一步, 在person实例中搜索, 搜索到直接返回或者调用函数. 如果没有执行第二步.
  • 第二步, 在Person的原型中搜索, Person的原型是谁? Animal的实例. 所以会在Animal的实例中搜索, 无论是属性还是方法, 如果搜索到则直接返回或者执行. 如果没有, 执行第三步.
  • 第三步, 在Animal的原型中搜索, 搜索到返回或者执行, 如果没有, 搜索结束. (当然其实还有Object, 但是先不考虑)
    画图解析可能更加清晰:
    当代码执行到第3步(上面代码的序号)的时候, 如图所示:

image.png

当代码执行第4步(上面代码的序号)时, 发生了如图所示的变化
注意图片中的红色线, 原来指向的是谁, 现在指向的是谁.

image.png

代码继续执行
Person.prototype.personFunction = function (){}
当执行第5步, 也就是给Person的原型赋值了一个函数时, 事实上在给new Animal(Animal的实例)赋值了一个新的方法.

image.png

代码继续执行, 我们创建了一个Person对象
创建Person对象, person对象会有自己的属性, personProperty.
另外, person对象有一个__prototype__指向Person的原型.
Person的原型是谁呢? 就是我们之前的new Animal(Animal的一个实例), 所以会指向它.
原型链简单总结:
通过实现原型链,本质上扩展了本章前面介绍的原型搜索机制。
当以读取模式访问一个实例属性时,首先会在实例中搜索该属性。
如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。
在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。

1.2. 原型和实例的关系

如果我们希望确定原型和实例之间的关系, 有两种方式:

  • 第一种方式是使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。
  • 第二种方式是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()方法也会返回true

instanceof操作符

// instanceof
alert(person instanceof Object) // true
alert(person instanceof Animal) // true
alert(person instanceof Person) // true

isPrototypeOf()函数

// isPrototypeOf函数
alert("isPrototypeOf函数函数")
alert(Object.prototype.isPrototypeOf(person)) // true
alert(Animal.prototype.isPrototypeOf(person)) // true
alert(Person.prototype.isPrototypeOf(person)) // true

1.3. 添加新的方法

添加新的方法
在第5步操作中, 我们为子类型添加了一个新的方法. 但是这里有一个注意点.
无论是子类中添加新的方法, 还是对父类中方法进行重写. 都一定要将添加方法的代码, 放在替换原型语句之后.
否则, 我们添加的方法将会无效. 错误代码引起的代码:

// 1.定义Animal的构造函数
function Animal() {
    this.animalProperty = "Animal"
}

// 2.给Animal添加方法
Animal.prototype.animalFunction = function () {
    alert(this.animalProperty)
}

// 3.定义Person的构造函数
function Person() {
    this.personProperty = "Person"
}

// 4.给Person添加方法
Person.prototype.personFunction = function () {
    alert(this.personProperty)
}

// 5.给Person赋值新的原型对象
Person.prototype = new Animal()

// 6.创建Person对象, 并且调用方法
var person = new Person()
person.personFunction() // 不会有任何弹窗, 因为找不到该方法

代码解析:
执行上面的代码不会出现任何的弹窗, 因为我们添加的方法是无效的, 被赋值的新的原型覆盖了.
正确的办法是将第4步和第5步操作换一下位置即可.