了解 JavaScript 中的原型和继承

介绍

JavaScript是一个基于原型的语言,这意味着对象属性和方法可以通过具有克隆和扩展能力的通用对象共享,这被称为原型遗传,并不同于类遗传。

在本教程中,我们将学习对象原型是什么以及如何使用构建函数来扩展原型到新的对象。

JavaScript 原型

理解JavaScript中的对象中,我们讨论了对象数据类型,如何创建对象,以及如何访问和修改对象属性。

JavaScript中的每个对象都有一个名为[原型]的内部属性,我们可以通过创建一个新的空对象来证明这一点。

1let x = {};

这是我们通常会创建一个对象的方式,但请注意,另一种方法是使用对象构建程序: let x = new Object()

<$>[注] 包含[Prototype]]的双方块表示它是一个内部属性,无法直接在代码中访问。

要找到这个新创建的对象的[Prototype],我们将使用getPrototypeOf()方法。

1Object.getPrototypeOf(x);

输出将包括几个内置的属性和方法。

1[secondary_label Output]
2{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, }

另一种找到[原型]的方法是通过__proto__属性。

<$>[注] 重要的是要注意, .__proto__ 是一个传统的功能,不应该用于生产代码,并且不存在于所有现代浏览器中。

1x.__proto__;

输出将与您使用getPrototypeOf()相同。

1[secondary_label Output]
2{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, }

重要的是,JavaScript中的每个对象都有一个[原型],因为它为任何两个或多个对象创建了一个链接的方式。

您创建的对象具有[原型],就像内置对象,如日期阵列一样。

原型遗产

当您尝试访问对象的属性或方法时,JavaScript 会先搜索对象本身,如果没有找到它,则会搜索对象的[Prototype]

原型链的尽头是 Object.prototype.所有对象都继承了 Object的属性和方法。

在我们的示例中,‘x’是一个从‘Object’继承的空对象。‘x’可以使用任何具有‘Object’的属性或方法,例如‘toString()’。

1x.toString();
1[secondary_label Output]
2[object Object]

这个原型链只有一个链条长. x -> Object. 我们知道这一点,因为如果我们试图链接两个 [Prototype]] 属性在一起,它将是 null

1x.__proto__.__proto__;
1[secondary_label Output]
2null

如果你有经验(https://www.digitalocean.com/community/tutorial_series/working-with-arrays-in-javascript),你知道他们有许多内置的方法,如pop()push()。当你创建一个新数组时,你可以访问这些方法的原因是因为你创建的任何数组都有访问Array.prototype上的属性和方法。

我们可以通过创建一个新的数组来测试这一点。

1let y = [];

请记住,我们也可以将其写成一个数组构建器, let y = new Array()

如果我们看看新的y数组的[原型],我们会看到它比x对象具有更多的属性和方法,它已经继承了Array.prototype的所有东西。

1y.__proto__;
1[constructor: ƒ, concat: ƒ, pop: ƒ, push: ƒ, ]

您将注意到原型上设置为Array()constructor属性,该constructor属性返回对象的constructor函数,这是从函数中构建对象的机制。

我们现在可以连接两个原型,因为我们的原型链在这种情况下更长,它看起来像y ->Array ->Object

1y.__proto__.__proto__;
1[secondary_label Output]
2{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, }

这个链现在是指Object.prototype。我们可以测试内部的[Prototype]与构建函数的prototype属性,看看它们是指相同的东西。

1y.__proto__ === Array.prototype;            // true
2y.__proto__.__proto__ === Object.prototype; // true

我们还可以使用isPrototypeOf()方法来实现这一点。

1Array.prototype.isPrototypeOf(y);      // true
2Object.prototype.isPrototypeOf(Array); // true

我们可以使用Instanceof运算器来测试构建者的原型属性是否出现在对象的原型链中。

1y instanceof Array; // true

要总结一下,所有JavaScript对象都有一个隐藏的内部[Prototype]属性(在某些浏览器中可以通过__proto__曝光)。

这些原型可以链接,每个额外的对象将继承整个链上的所有东西。

建设者功能

Constructor 函数是用于构建新对象的函数. new 操作员用于创建基于构建函数的新实例. 我们已经看到一些内置的 JavaScript 构建程序,如 new Array()new Date(),但我们还可以创建自己的自定义模板来构建新对象。

例如,假设我们正在创建一个非常简单的,基于文本的角色扮演游戏. 用户可以选择一个字符,然后选择他们将有哪些字符类别,如战士,治愈者,盗贼等。

由于每个字符将共享许多特征,例如拥有一个名字,一个级别和点击点,所以创建一个构建器作为一个模板是有意义的. 然而,由于每个字符类可能有非常不同的能力,我们希望确保每个字符只有访问自己的能力. 让我们看看我们如何通过原型继承和构建器实现这一点。

首先,构建函数只是一个常规函数,当它被一个具有关键字的实例调用时,它就会成为构建函数。

1[label characterSelect.js]
2// Initialize a constructor function for a new Hero
3function Hero(name, level) {
4  this.name = name;
5  this.level = level;
6}

我们创建了一个名为英雄的构造函数,有两个参数:名称级别。由于每个字符都会有一个名称和一个级别,所以每个新字符都应该具有这些属性。

现在我们可以创建一个与的新实例。

1let hero1 = new Hero('Bjorn', 1);

如果我们将hero1排除,我们将看到一个新的对象已经创建,新的属性设置如预期。

1[secondary_label Output]
2Hero {name: "Bjorn", level: 1}

现在,如果我们得到hero1[原型],我们将能够看到constructor作为Hero()(记住,这与hero1.__proto__具有相同的输入,但这是正确的使用方法)。

1Object.getPrototypeOf(hero1);
1[secondary_label Output]
2constructor: ƒ Hero(name, level)

您可能会注意到,我们在构建器中只定义了属性,而不是方法,在原型上定义方法是JavaScript中常见的做法,以提高效率和代码可读性。

我们可以使用原型添加一个方法到英雄,我们将创建一个greet()方法。

1[label characterSelect.js]
2...
3// Add greet method to the Hero prototype
4Hero.prototype.greet = function () {
5  return `${this.name} says hello.`;
6}

由于greet()Hero原型中,而hero1Hero的实例,因此该方法可用于hero1

1hero1.greet();
1[secondary_label Output]
2"Bjorn says hello."

如果您检查 Hero 的 [原型],您将看到 greet()` 作为现在可用的选项。

这是好事,但现在我们想要创建英雄使用的字符类。将每个类的所有能力放入英雄构造器中是毫无意义的,因为不同的类将有不同的能力。

我们可以使用 call()方法将属性从一个建设者复制到另一个建设者。

 1[label characterSelect.js]
 2...
 3// Initialize Warrior constructor
 4function Warrior(name, level, weapon) {
 5  // Chain constructor with call
 6  Hero.call(this, name, level);
 7
 8  // Add a new property
 9  this.weapon = weapon;
10}
11
12// Initialize Healer constructor
13function Healer(name, level, spell) {
14  Hero.call(this, name, level);
15
16  this.spell = spell;
17}

现在,这两种新构造器都具有英雄的属性和一些不合适的属性,我们将将攻击( )方法添加到战士中,而治愈( )方法添加到愈合中。

1[label characterSelect.js]
2...
3Warrior.prototype.attack = function () {
4  return `${this.name} attacks with the ${this.weapon}.`;
5}
6
7Healer.prototype.heal = function () {
8  return `${this.name} casts ${this.spell}.`;
9}

在此时刻,我们将用两个可用的新字符类来创建我们的字符。

1[label characterSelect.js]
2const hero1 = new Warrior('Bjorn', 1, 'axe');
3const hero2 = new Healer('Kanin', 1, 'cure');

hero1现在被认定为战士与新的属性。

1[secondary_label Output]
2Warrior {name: "Bjorn", level: 1, weapon: "axe"}

我们可以使用我们在战士原型上设置的新方法。

1hero1.attack();
1[secondary_label Console]
2"Bjorn attacks with the axe."

但是,如果我们试图在原型链上进一步使用方法,会发生什么?

1hero1.greet();
1[secondary_label Output]
2Uncaught TypeError: hero1.greet is not a function

当你使用call()来连接链构造器时,原型属性和方法不会自动关联,我们将使用Object.setPropertyOf()英雄构造器中的属性连接到战士愈合构造器,确保将其放在任何其他方法之前。

1[label characterSelect.js]
2...
3Object.setPrototypeOf(Warrior.prototype, Hero.prototype);
4Object.setPrototypeOf(Healer.prototype, Hero.prototype);
5
6// All other prototype methods added below
7...

现在我们可以成功地使用从英雄的原型方法在一个战士治疗者的实例上。

1hero1.greet();
1[secondary_label Output]
2"Bjorn says hello."

这里是我们角色创建页面的完整代码。

 1[label characterSelect.js]
 2// Initialize constructor functions
 3function Hero(name, level) {
 4  this.name = name;
 5  this.level = level;
 6}
 7
 8function Warrior(name, level, weapon) {
 9  Hero.call(this, name, level);
10
11  this.weapon = weapon;
12}
13
14function Healer(name, level, spell) {
15  Hero.call(this, name, level);
16
17  this.spell = spell;
18}
19
20// Link prototypes and add prototype methods
21Object.setPrototypeOf(Warrior.prototype, Hero.prototype);
22Object.setPrototypeOf(Healer.prototype, Hero.prototype);
23
24Hero.prototype.greet = function () {
25  return `${this.name} says hello.`;
26}
27
28Warrior.prototype.attack = function () {
29  return `${this.name} attacks with the ${this.weapon}.`;
30}
31
32Healer.prototype.heal = function () {
33  return `${this.name} casts ${this.spell}.`;
34}
35
36// Initialize individual character instances
37const hero1 = new Warrior('Bjorn', 1, 'axe');
38const hero2 = new Healer('Kanin', 1, 'cure');

借助此代码,我们创建了我们的英雄构造器与基本属性,从原始构造器创建了两个名为战士愈合者的字符构造器,向原型添加了方法,并创建了单个字符实例。

结论

JavaScript是一个基于原型的语言,它与许多其他面向对象的语言使用的传统类型范式不同。

在本教程中,我们了解了原型如何在JavaScript中工作,以及如何通过所有对象共享的隐藏[Prototype]]属性链接对象属性和方法,我们还了解了如何创建自定义构建函数以及原型继承如何传递属性和方法值。

Published At
Categories with 技术
comments powered by Disqus