了解JavaScript中的原型和继承

JavaScript是一种基于原型的语言,这意味着对象属性和方法可以通过具有克隆和扩展能力的通用对象来共享。这被称为原型继承,不同于类继承。在受欢迎的...

介绍

JavaScript是一种基于原型的语言 ,这意味着对象属性和方法可以通过具有克隆和扩展能力的通用对象来共享。 这被称为原型继承,不同于类继承。 在流行的面向对象编程语言中,JavaScript是相对独特的,因为其他主要语言(如PHP,Python和Java)都是基于类的语言,而将类定义为对象的蓝图。

在本教程中,我们将学习什么是对象原型,以及如何使用构造函数将原型扩展到新对象中。 我们也将学习继承和原型链。

JavaScript原型

在“ 了解JavaScript中的对象”中 ,我们介绍了对象数据类型,如何创建对象以及如何访问和修改对象属性。 现在我们将学习如何使用原型来扩展对象。

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

let x = {};

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

包含[[Prototype]]的双方括号表示它是内部属性,不能直接在代码中访问。

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

Object.getPrototypeOf(x);

输出将由几个内置的属性和方法组成。

{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

找到[[Prototype]]另一种方法是通过__proto__属性。 __proto__是一个暴露对象内部[[Prototype]]的属性。

重要的是要注意.__proto__是一个遗留功能,不应该被用在生产代码中,并且它不会在每个现代浏览器中出现。 不过,我们可以在整篇文章中使用它作为演示的目的。

x.__proto__;

输出将与您使用getPrototypeOf()

{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

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

您创建的对象具有[[Prototype]] ,就像内置对象(如DateArray 通过prototype属性可以引用这个内部属性从一个对象到另一个对象,我们将在本教程后面看到。

原型继承

当你试图访问一个对象的属性或方法时,JavaScript将首先在对象本身上进行搜索,如果找不到,它将搜索对象的[[Prototype]] 如果在查询对象及其[[Prototype]]仍未找到匹配项,JavaScript将检查链接对象的原型,并继续搜索,直至到达原型链的末尾。

在原型链的末尾是Object.prototype 所有对象都继承了Object的属性和方法。 任何超出链末端的搜索都会导致null

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

x.toString();
[object Object]

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

x.__proto__.__proto__;
null

我们来看看另一种类型的对象。 如果你有经验在JavaScript中使用数组 ,你知道他们有很多内置的方法,比如pop()push() 创建新数组时,您可以访问这些方法的原因是因为您创建的任何数组都有权访问Array.prototype上的属性和方法。

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

let y = [];

请记住,我们也可以把它写成一个数组的构造函数, let y = new Array()

如果我们看一下新的y数组的[[Prototype]] ,我们会看到它比x对象有更多的属性和方法。 它继承了Array.prototype所有内容。

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

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

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

y.__proto__.__proto__;
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

这个链现在指的是Object.prototype 我们可以对构造函数的prototype属性测试internal [[Prototype]] ,看看它们是指同一个东西。

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

我们也可以使用isPrototypeOf()方法来完成这个任务。

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

我们可以使用instanceof运算符来测试构造函数的prototype属性是否出现在对象的原型链的任何地方。

y instanceof Array; // true

总而言之,所有JavaScript对象都有一个隐藏的内部[[Prototype]]属性(可能会在某些浏览器中通过__proto__公开)。 对象可以被扩展,并将继承构造函数的[[Prototype]]上的属性和方法。

这些原型可以链接在一起,每个附加对象将继承整个链中的所有内容。 链以Object.prototype结束。

构造函数

构造函数是用来构造新对象的函数。 new运算符用于基于构造函数创建新实例。 我们已经看到了一些内置的JavaScript构造函数,比如new Array()new Date() ,但是我们也可以创建自己的自定义模板来构建新的对象。

举个例子,假设我们正在创建一个非常简单的基于文本的角色扮演游戏。 用户可以选择一个角色,然后选择他们将拥有的角色类别,如战士,治疗者,小偷等等。

由于每个角色都会共享很多特征,比如有一个名字,一个关卡和一个生命值,所以创建一个构造函数作为模板是有意义的。 然而,由于每个角色的类别可能有很大的不同,所以我们要确保每个角色只能使用自己的能力。 让我们来看看如何用原型继承和构造函数来完成这个任务。

首先,构造函数只是一个常规函数。 当它由new关键字实例调用时,它将成为构造函数。 在JavaScript中,我们按照惯例大写构造函数的第一个字母。

characterSelect.js
// Initialize a constructor function for a new Hero
function Hero(name, level) {
  this.name = name;
  this.level = level;
}

我们创建了一个名为Hero的构造函数,它有两个参数: namelevel 因为每个角色都有一个名字和一个等级,所以每个新角色都有这些属性。 this关键字将引用创建的新实例,所以将this.name设置为name参数可确保新对象具有name属性集。

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

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

如果我们调出hero1 ,我们会看到一个新的对象已经被创建,新的属性被设置为预期的。

Hero {name: "Bjorn", level: 1}

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

Object.getPrototypeOf(hero1);
constructor: ƒ Hero(name, level)

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

我们可以使用prototype添加一个方法给Hero 我们将创建一个greet()方法。

characterSelect.js
...
// Add greet method to the Hero prototype
Hero.prototype.greet = function () {
  return `${this.name} says hello.`;
}

由于greet()Heroprototype中,而hero1Hero一个实例,所以该方法可用于hero1

hero1.greet();
"Bjorn says hello."

如果你检查Hero的[[Prototype]] ,现在你会看到greet()作为可用选项。

这很好,但现在我们要为英雄们创造角色类。 将每个类的所有能力都放到Hero构造函数中是没有意义的,因为不同的类会有不同的能力。 我们想创建新的构造函数,但是我们也想让它们连接到原来的Hero

我们可以使用call()方法将一个构造函数的属性复制到另一个构造函数中。 我们来创建一个Warrior和一个Healer的构造函数。

characterSelect.js
...
// Initialize Warrior constructor
function Warrior(name, level, weapon) {
  // Chain constructor with call
  Hero.call(this, name, level);

  // Add a new property
  this.weapon = weapon;
}

// Initialize Healer constructor
function Healer(name, level, spell) {
  Hero.call(this, name, level);

  this.spell = spell;
}

现在两个新的构造函数都具有“ Hero和几个独特的属性。 我们将attack()方法添加到Warriorheal()方法添加到Healer

[label characterSelect.js
...
Warrior.prototype.attack = function () {
  return `${this.name} attacks with the ${this.weapon}.`;
}

Healer.prototype.heal = function () {
  return `${this.name} casts ${this.spell}.`;
}

在这一点上,我们将使用两个可用的新字符类创建我们的角色。

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

hero1现在被认为是具有新属性的Warrior

Warrior {name: "Bjorn", level: 1, weapon: "axe"}

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

hero1.attack();
"Bjorn attacks with the axe."

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

hero1.greet();
Uncaught TypeError: hero1.greet is not a function

当您使用call()来链式构造函数时,原型属性和方法不会自动链接。 我们将使用Object.create()来链接原型,确保在创建任何附加方法之前将其添加到原型中。

characterSelect.js
...
Warrior.prototype = Object.create(Hero.prototype);
Healer.prototype = Object.create(Hero.prototype);

// All other prototype methods added below
...

现在我们可以成功地使用Hero原型方法在一个WarriorHealer的实例。

hero1.greet();
"Bjorn says hello."

以下是我们角色创建页面的完整代码。

characterSelect.js
// Initialize constructor functions
function Hero(name, level) {
  this.name = name;
  this.level = level;
}

function Warrior(name, level, weapon) {
  Hero.call(this, name, level);

  this.weapon = weapon;
}

function Healer(name, level, spell) {
  Hero.call(this, name, level);

  this.spell = spell;
}

// Link prototypes and add prototype methods
Warrior.prototype = Object.create(Hero.prototype);
Healer.prototype = Object.create(Hero.prototype);

Hero.prototype.greet = function () {
  return `${this.name} says hello.`;
}

Warrior.prototype.attack = function () {
  return `${this.name} attacks with the ${this.weapon}.`;
}

Healer.prototype.heal = function () {
  return `${this.name} casts ${this.spell}.`;
}

// Initialize individual character instances
const hero1 = new Warrior('Bjorn', 1, 'axe');
const hero2 = new Healer('Kanin', 1, 'cure');

使用这段代码,我们创建了具有基本属性的Hero类,从原始构造函数创建了两个名为WarriorHealer字符类,向原型添加了方法并创建了单个字符实例。

结论

JavaScript是一种基于原型的语言,其功能与传统的基于类的范例不同,许多其他面向对象的语言都使用这种语言。

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