子类
实现继承的好处之一就是它允许你重用现有代码。通过建立继承,我们可以子类化,也就是让一个“子”对象接受“父”对象的大部分或全部属性和方法,同时保留它自己独特的方法。
假设我们有一个父 Animal
对象(即构造函数),其中包含诸如 age
和 weight
等属性。同一个 Animal
对象还可以访问 eat
和 sleep
等方法。
现在,再假设我们要创建一个 Cat
子对象。与描述其他动物一样,你也可以通过 age
或 weight
来描述一只猫,而且你也可以确定猫会 eat
和 sleep
。因此,在创建这个 Cat
对象的时候,我们可以简单地重写和重新实现 Animal
中的所有方法和属性——或者,我们也可以让 Cat
从 Animal
继承 这些现有的属性和方法,从而节省时间并防止重复代码!
我们不仅可以让 Cat
接受 Animal
的属性和方法,还可以赋予 Cat
独特的属性和方法!也许一只 Cat
具有独特的 lives
属性为 9,或有一个专门的 meow()
方法,是其他 Animal
所没有的。使用原型继承,Cat
只需要实现 Cat
的独特功能,并重用 Animal
的现有功能即可。
通过原型继承
回想一下上一部分的原型链:
当在任何对象上调用任何属性时,JavaScript 引擎将首先在该对象中查找该属性(即该对象自己的、非继承的属性)。如果没有找到该属性,JavaScript 将查看该对象的原型。如果在对象原型中 仍然 找不到该属性,则 JavaScript 将在原型链上继续搜索。
JavaScript 中的继承重点就是建立原型链。
秘密链接
如你所知,在尝试搜索一个不存在于某个对象中的属性时,该对象的构造函数的原型是被首先搜索的。请考虑以下具有两个属性 claws
和 diet
的 bear
对象:
const bear = { |
让我们将以下 PolarBear()
构造函数的 prototype
属性赋为 bear
:
function PolarBear() { |
现在,让我们调用 PolarBear() 构造函数来创建一个新的对象,然后给它两个属性:
const snowball = new PolarBear(); |
snowball
对象目前看起来像这样:
{ |
请注意,snowball
只有两个自己的属性:color
和 favoriteDrink
。但是,snowball
也可以访问自身并不具有的属性:claws
和 diet
:
console.log(snowball.claws); |
由于 claw 和 diet 都作为 prototype
对象中的属性存在,因此它们会被查找,因为对象被秘密链接到其构造函数的 prototype
属性。
太棒了!但你可能会想:这个通向 prototype
对象的秘密链接到底是什么呢?当从 PolarBear()
构造函数构造对象之后(如 snowball
),这些对象可以立即访问 PolarBear()
的原型中的属性。这究竟是怎么做到的呢?
事实证明,这个秘密链接是 snowball
的 __proto__
属性(注意每一端有两个下划线)。
__proto__
是构造函数所创建的所有对象(即实例)的一个属性,并直接指向该构造函数的 prototype
对象。让我们来看看它是什么样的!
console.log(snowball.__proto__); |
由于 __proto__
属性所指向的对象与 PolarBear
的原型 bear
相同,因此将它们进行比较会返回 true
:
console.log(snowball.__proto__ === bear); |
强烈建议不要重新分配 proto 属性,甚至不要在你编写的任何代码中使用它。
首先,会有跨浏览器的兼容性问题。
更重要的是:由于 JavaScript 引擎会在原型链上搜索和访问属性,因此更改对象的原型可能会导致性能问题。
有关 proto 的 MDN 文章甚至警告,不要在页面顶部的红色文本中使用此属性!
我们有必要知道这个秘密链接,以了解函数和对象是如何相互关联的,但你不应该使用 __proto__
来管理继承。如果你只是需要查看对象的原型,则仍然可以使用 Object.getPrototypeOf()
来达到目的。
如果只继承原型呢?
假设我们希望一个
Child
对象从一个Parent
对象继承。为什么不应该只设置Child.prototype = Parent.prototype
呢?
首先,还记得吗,对象是通过引用来传递的。这意味着,由于
Child.prototype
对象和Parent.prototype
对象引用的是同一个对象,因此你对Child
的原型所作的任何更改也会被应用于Parent
的原型!我们可不希望子对象能够修改其父对象的属性!
最重要的是,这样做不会创建原型链。如果我们想让一个对象从我们想要的任何对象进行继承,而不仅仅是它的原型呢?
我们仍然需要一种方式来有效地管理继承,同时又完全不会改变原型。
小练习
请考虑以下代码:
function GuineaPig (name) { |
waffle.__proto__
指向什么?
参考答案: GuineaPig.prototype
请考虑以下代码:
function Car (color, year) { |
当 car.drive()
被调用时,会发生什么?
参考答案:
顺序 | 事件 |
---|---|
第一 | JavaScript引擎在car 对象内搜索名为drive 的属性 |
第二 | JavaScript引擎在car 对象中找不到drive 属性 |
第三 | 然后,JavaScript引擎会访问car.__proto__ 属性 |
第四 | 由于car.__proto__ 指向Car.prototype ,因此JavaScript引擎会在该原型中搜索drive |
第五 | 由于Car.prototype.drive 是一个已定义的属性,因此它会被返回 |
第六 | 最后,由于drive 作为一个方法在car 上被调用,因此this 的值会被设置为car |
Object.create()
到目前为止,我们在继承方面遇到了一些问题。首先,虽然 __proto__
可以访问被调用的对象的原型,但是在你编写的代码中使用它并不是好习惯。
另一方面,我们也不应该_只_继承原型;这样做不会创建原型链,而且我们对子对象所作的任何更改也会反映在父对象中。
那么,我们应该如何继续往前呢?
实际上,我们可以借助一种方式来自己设置对象的原型:使用 Object.create()
。而且最棒的是,这种方式既可以让我们管理继承,同时又 不会 改变原型!
Object.create()
会接受一个对象作为参数,并返回一个新的对象,其 __proto__
属性会被设置为传递给它的参数。然后,你只需要将所返回的对象设置为子对象构造函数的原型即可。让我们来看一个例子!
首先,假设我们有一个 mammal
对象,它有两个属性:vertebrate
和 earBones
:
const mammal = { |
还记得吗,Object.create()
会接受一个对象作为参数,并返回一个 新的 对象。这个新对象的 __proto__
属性会被设置为最初传递给 Object.create()
的参数。让我们把这个返回值保存到变量 rabbit
中:
const rabbit = Object.create(mammal); |
我们预期这个新的 rabbit
对象是空白的,没有自己的属性:
console.log(rabbit); |
但是,rabbit
现在应该已被秘密链接到 mammal
。也就是说,它的 __proto__
属性应该指向 mammal
:
console.log(rabbit.__proto__ === mammal); |
太棒了!这意味着,现在 rabbit
扩展了 mammal
(即 rabbit
继承自 mammal
),而且 rabbit
可以将 mammal
的属性当作自己的属性一样进行访问!
console.log(rabbit.vertebrate); |
Object.create()
给了我们一个在 JavaScript 中建立原型继承的简洁方法。我们可以通过这种方式轻松扩展原型链,而且可以让对象从我们想要的任何对象进行继承!
下面让我们来看一个更复杂的例子:
function Animal (name) { |
这是一个 Animal 构造函数,以及一个在 Animal 的原型上直接定义的 walk 方法,此外,还有一个 Cat 构造函数,你可能注意到,我们在 Cat 构造函数中使用了 call 方法,并且直接在 Animal 构造函数 中调用它。
我们使用 call 而不是关键字 new ,因为我们不想构造一个全新的 animal 对象。我们只关心 cat 实例或 cat 对象上的 animal 初始化逻辑,call 方法的作用是调用 Animal 并将 this 设为 cat 实例,否则 this.name
将是 undefined
。
现在,有了这个继承自 Animal 的 Cat:
Cat.prototype = Object.create(Animal.prototype); |
我们还需要更改构造函数, 否则所有的 cat 对象的构造函数将设为 animal:
Cat.prototype.constructor = Cat; |
现在,我们向 cat 的原型添加一个 使所有 cat 对象都能共享的方法: Meow
Cat.prototype.meow = function () { |
使用关键字 new 调用 cat 构造函数,并初始化新的 cat 对象 称之为 Bambi:
const bambi = new Cat('Bambi'); |
总结下,为了在JavaScript 中有效地管理继承,一个很好的方式是避免完全更改原型 ,为此,我们可以使用
object.create
小练习
请考虑以下代码:
function Parent() { |
当 child instanceof Parent;
被执行时,什么会被输出到控制台?
参考答案: true
以下哪一项有关
Object.create()
的说法是正确的?请选择所有适用项:
- 它会返回一个新的对象,其
__proto__
属性会被设置为传递给Object.create()
的对象 - 使用
Object.create()
,我们可以让对象从我们想要的任何对象进行继承(即不仅是prototype
) Object.create()
让我们既可以实现原型继承,又不会改变原型- 该方法直接在一个对象上被调用
参考答案: 1、2、3
小结
JavaScript 中的继承重点就是建立原型链。这让我们可以子类化,也就是创建一个“子”对象,让它继承“父”对象的大部分或全部属性和方法。然后,我们可以分别实现任何子对象的独特属性和方法,同时仍然保留其父对象的数据和功能。
对象(实例)通过该实例的 __proto__
属性被秘密链接到其构造函数的原型对象。 你不应该在你编写的任何代码中使用 __proto__
。在任何代码中使用 __proto__
,或者只继承原型,将会直接导致某些不必要的问题。要在 JavaScript 中高效地管理继承,一个有效的方式就是避免完全改变原型。
Object.create()
可以帮助我们做到这一点,它可以接受一个父对象,返回一个 新的 对象,并将其 __proto__
属性设置为该父对象。
延伸
MDN 上的继承和原型链
MDN 上的Object.create()