原型继承:子类

子类


实现继承的好处之一就是它允许你重用现有代码。通过建立继承,我们可以子类化,也就是让一个“子”对象接受“父”对象的大部分或全部属性和方法,同时保留它自己独特的方法。

假设我们有一个父 Animal 对象(即构造函数),其中包含诸如 ageweight 等属性。同一个 Animal 对象还可以访问 eatsleep 等方法。

现在,再假设我们要创建一个 Cat 子对象。与描述其他动物一样,你也可以通过 ageweight 来描述一只猫,而且你也可以确定猫会 eatsleep。因此,在创建这个 Cat 对象的时候,我们可以简单地重写和重新实现 Animal 中的所有方法和属性——或者,我们也可以让 CatAnimal 继承 这些现有的属性和方法,从而节省时间并防止重复代码!

我们不仅可以让 Cat 接受 Animal 的属性和方法,还可以赋予 Cat 独特的属性和方法!也许一只 Cat 具有独特的 lives 属性为 9,或有一个专门的 meow() 方法,是其他 Animal 所没有的。使用原型继承,Cat 只需要实现 Cat 的独特功能,并重用 Animal 的现有功能即可。

通过原型继承


回想一下上一部分的原型链:

原型链

当在任何对象上调用任何属性时,JavaScript 引擎将首先在该对象中查找该属性(即该对象自己的、非继承的属性)。如果没有找到该属性,JavaScript 将查看该对象的原型。如果在对象原型中 仍然 找不到该属性,则 JavaScript 将在原型链上继续搜索。

JavaScript 中的继承重点就是建立原型链。

秘密链接


如你所知,在尝试搜索一个不存在于某个对象中的属性时,该对象的构造函数的原型是被首先搜索的。请考虑以下具有两个属性 clawsdietbear 对象:

const bear = {
claws: true,
diet: 'carnivore'
};

让我们将以下 PolarBear() 构造函数的 prototype 属性赋为 bear

function PolarBear() {
//...
}

PolarBear.prototype = bear;

现在,让我们调用 PolarBear() 构造函数来创建一个新的对象,然后给它两个属性:

const snowball = new PolarBear();

snowball.color = 'white';

snowball.favoriteDrink = 'cola';

snowball 对象目前看起来像这样:

{
color: 'white',
favoriteDrink: 'cola'
}

请注意,snowball 只有两个自己的属性:colorfavoriteDrink。但是,snowball 也可以访问自身并不具有的属性:clawsdiet

console.log(snowball.claws);
//true

console.log(snowball.diet);
//'carnivore'

由于 claw 和 diet 都作为 prototype 对象中的属性存在,因此它们会被查找,因为对象被秘密链接到其构造函数的 prototype 属性。

太棒了!但你可能会想:这个通向 prototype 对象的秘密链接到底是什么呢?当从 PolarBear() 构造函数构造对象之后(如 snowball),这些对象可以立即访问 PolarBear() 的原型中的属性。这究竟是怎么做到的呢?

事实证明,这个秘密链接是 snowball__proto__ 属性(注意每一端有两个下划线)。

__proto__ 是构造函数所创建的所有对象(即实例)的一个属性,并直接指向该构造函数的 prototype 对象。让我们来看看它是什么样的!

console.log(snowball.__proto__);

//{ claws: true, diet: 'carnivore' }

由于 __proto__ 属性所指向的对象与 PolarBear 的原型 bear 相同,因此将它们进行比较会返回 true

console.log(snowball.__proto__ === bear);

//true

强烈建议不要重新分配 proto 属性,甚至不要在你编写的任何代码中使用它。


首先,会有跨浏览器的兼容性问题。
更重要的是:由于 JavaScript 引擎会在原型链上搜索和访问属性,因此更改对象的原型可能会导致性能问题。
有关 proto 的 MDN 文章甚至警告,不要在页面顶部的红色文本中使用此属性!

我们有必要知道这个秘密链接,以了解函数和对象是如何相互关联的,但你不应该使用 __proto__ 来管理继承。如果你只是需要查看对象的原型,则仍然可以使用 Object.getPrototypeOf() 来达到目的。


如果只继承原型呢?


假设我们希望一个 Child 对象从一个 Parent 对象继承。为什么不应该只设置 Child.prototype = Parent.prototype 呢?

首先,还记得吗,对象是通过引用来传递的。这意味着,由于 Child.prototype 对象和 Parent.prototype 对象引用的是同一个对象,因此你对 Child 的原型所作的任何更改也会被应用于 Parent 的原型!我们可不希望子对象能够修改其父对象的属性!

最重要的是,这样做不会创建原型链。如果我们想让一个对象从我们想要的任何对象进行继承,而不仅仅是它的原型呢?

我们仍然需要一种方式来有效地管理继承,同时又完全不会改变原型。

小练习


请考虑以下代码:

function GuineaPig (name) {
this.name = name;
this.isCute = true;
}

const waffle = new GuineaPig('Waffle');

waffle.__proto__指向什么?

参考答案: GuineaPig.prototype

请考虑以下代码:

function Car (color, year) {
this.color = color;
this.year = year;
}

Car.prototype.drive = function () {
console.log('Vroom vroom!');
};

const car = new Car('silver', 1988);

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 对象,它有两个属性:vertebrateearBones

const mammal = {
vertebrate: true,
earBones: 3
};

还记得吗,Object.create() 会接受一个对象作为参数,并返回一个 新的 对象。这个新对象的 __proto__ 属性会被设置为最初传递给 Object.create() 的参数。让我们把这个返回值保存到变量 rabbit 中:

const rabbit = Object.create(mammal);

我们预期这个新的 rabbit 对象是空白的,没有自己的属性:

console.log(rabbit);

//{}

但是,rabbit 现在应该已被秘密链接到 mammal。也就是说,它的 __proto__ 属性应该指向 mammal

console.log(rabbit.__proto__ === mammal);

//true

太棒了!这意味着,现在 rabbit 扩展了 mammal(即 rabbit 继承自 mammal),而且 rabbit 可以将 mammal 的属性当作自己的属性一样进行访问!

console.log(rabbit.vertebrate);
//true

console.log(rabbit.earBones);
//3

Object.create() 给了我们一个在 JavaScript 中建立原型继承的简洁方法。我们可以通过这种方式轻松扩展原型链,而且可以让对象从我们想要的任何对象进行继承!

下面让我们来看一个更复杂的例子:

function Animal (name) {
this.name = name;
}

Animal.prototype.walk = function () {
console.log(`${this.name} walks!`);
};

function Cat (name) {
Animal.call(this, name);
this.lives = 9;
}

这是一个 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);
// Animal{}

我们还需要更改构造函数, 否则所有的 cat 对象的构造函数将设为 animal:

Cat.prototype.constructor = Cat;

// f Cat(name){
// Animal.call(this, name);
// this.lives = 9;
// }

现在,我们向 cat 的原型添加一个 使所有 cat 对象都能共享的方法: Meow

Cat.prototype.meow = function () {
console.log('Meow!');
};

// f () {
// console.log('Meow!');
// }

使用关键字 new 调用 cat 构造函数,并初始化新的 cat 对象 称之为 Bambi:

const bambi = new Cat('Bambi');

bambi.meow(); // Meow!
bambi.walk(); // Bambi walks!

bambi.name; // "Bambi"

总结下,为了在JavaScript 中有效地管理继承,一个很好的方式是避免完全更改原型 ,为此,我们可以使用 object.create

小练习


请考虑以下代码:

function Parent() {
// ...
}

function Child() {
// ...
}

Child.prototype = Object.create(Parent.prototype);

const child = new Child();

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__ 属性设置为该父对象。

延伸


-------------本文结束 感谢您的阅读-------------
0%