了解JavaScript中的原型和继承
介绍
JavaScript是一种基于原型的语言,这意味着对象属性和方法可以通过具有克隆和扩展能力的通用对象来共享。 这称为原型继承,与类继承不同。 在流行的面向对象编程语言中,JavaScript 是比较独特的,因为 PHP、Python 和 Java 等其他著名语言都是基于类的语言,它们将类定义为对象的蓝图。
在本教程中,我们将学习什么是对象原型以及如何使用构造函数将原型扩展为新对象。 我们还将学习继承和原型链。
JavaScript 原型
在 Understanding Objects in JavaScript 中,我们回顾了对象数据类型、如何创建对象以及如何访问和修改对象属性。 现在我们将学习如何使用原型来扩展对象。
JavaScript 中的每个对象都有一个名为 Prototype
的内部属性。 我们可以通过创建一个新的空对象来证明这一点。
let x = {};
这是我们通常创建对象的方式,但请注意,另一种实现方式是使用对象构造函数:let x = new Object()
。
Prototype
括起来的双方括号表示它是一个内部属性,不能在代码中直接访问。
要找到这个新创建的对象的 Prototype
,我们将使用 getPrototypeOf()
方法。
Object.getPrototypeOf(x);
输出将包含几个内置属性和方法。
Output{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
查找 Prototype
的另一种方法是通过 __proto__
属性。 __proto__ 是一个暴露对象内部 Prototype
的属性。
需要注意的是,.__proto__
是一项遗留功能,不应在生产代码中使用,并且并非在每个现代浏览器中都存在。 但是,我们可以在整篇文章中使用它来进行演示。
x.__proto__;
输出将与您使用 getPrototypeOf()
时相同。
Output{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
JavaScript 中的每个对象都有一个 Prototype
是很重要的,因为它为任何两个或多个对象创建了一种链接方式。
您创建的对象具有 Prototype
,内置对象也具有 Date
和 Array
。 可以通过 prototype
属性从一个对象到另一个对象引用这个内部属性,我们将在本教程后面看到。
原型继承
当你试图访问一个对象的属性或方法时,JavaScript 会首先搜索对象本身,如果没有找到,它会搜索对象的 Prototype
。 如果在查询对象及其 Prototype
后仍然没有找到匹配项,JavaScript 将检查链接对象的原型,并继续搜索直到到达原型链的末尾。
原型链的末端是 Object.prototype。 所有对象都继承Object的属性和方法。 任何超出链末尾的搜索尝试都会导致 null
。
在我们的示例中,x
是一个从 Object
继承的空对象。 x
可以使用 Object
具有的任何属性或方法,例如 toString()
。
x.toString();
Output[object Object]
这个原型链只有一个环节长。 x
-> Object
。 我们知道这一点,因为如果我们尝试将两个 Prototype
属性链接在一起,它将是 null
。
x.__proto__.__proto__;
Outputnull
让我们看看另一种类型的对象。 如果你有 Working with Arrays in JavaScript 的经验,就会知道它们有很多内置方法,例如 pop()
和 push()
。 创建新数组时可以访问这些方法的原因是因为您创建的任何数组都可以访问 Array.prototype
上的属性和方法。
我们可以通过创建一个新数组来测试它。
let y = [];
请记住,我们也可以将其编写为数组构造函数,let y = new Array()
。
如果我们看一下新的 y
数组的 Prototype
,我们会发现它比 x
对象具有更多的属性和方法。 它继承了Array.prototype
的一切。
y.__proto__;
[constructor: ƒ, concat: ƒ, pop: ƒ, push: ƒ, …]
您会注意到原型上的 constructor
属性设置为 Array()
。 constructor
属性返回对象的构造函数,这是一种用于从函数构造对象的机制。
我们现在可以将两个原型链接在一起,因为在这种情况下我们的原型链更长。 它看起来像 y
-> Array
-> Object
。
y.__proto__.__proto__;
Output{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
该链现在指的是 Object.prototype
。 我们可以根据构造函数的 prototype
属性来测试内部的 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 中,我们按照惯例将构造函数的第一个字母大写。
字符选择.js
// Initialize a constructor function for a new Hero function Hero(name, level) { this.name = name; this.level = level; }
我们创建了一个名为 Hero
的构造函数,它有两个参数:name
和 level
。 由于每个角色都有名称和级别,因此每个新角色都具有这些属性是有意义的。 this
关键字将引用创建的新实例,因此将 this.name
设置为 name
参数可确保新对象具有 name
属性集.
现在我们可以使用 new
创建一个新实例。
let hero1 = new Hero('Bjorn', 1);
如果我们控制台输出 hero1
,我们将看到一个新对象已创建,并按预期设置了新属性。
OutputHero {name: "Bjorn", level: 1}
现在,如果我们得到 hero1
的 Prototype
,我们将能够看到 constructor
为 Hero()
。 (请记住,这与 hero1.__proto__
具有相同的输入,但使用方法是正确的。)
Object.getPrototypeOf(hero1);
Outputconstructor: ƒ Hero(name, level)
您可能会注意到我们在构造函数中只定义了属性而不是方法。 JavaScript 中的一种常见做法是在原型上定义方法以提高效率和代码可读性。
我们可以使用 prototype
向 Hero
添加一个方法。 我们将创建一个 greet()
方法。
字符选择.js
... // Add greet method to the Hero prototype Hero.prototype.greet = function () { return `${this.name} says hello.`; }
由于greet()
在Hero
的prototype
中,而hero1
是Hero
的一个实例,所以该方法对hero1
。
hero1.greet();
Output"Bjorn says hello."
如果您检查 Hero 的 Prototype
,您会看到 greet()
现在是可用选项。
这很好,但现在我们要创建角色类供英雄使用。 将每个类的所有能力都放入 Hero
构造函数是没有意义的,因为不同的类将具有不同的能力。 我们想创建新的构造函数,但我们也希望它们连接到原来的 Hero
。
我们可以使用 call() 方法将属性从一个构造函数复制到另一个构造函数。 让我们创建一个 Warrior 和一个 Healer 构造函数。
字符选择.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()
方法添加到 Warrior
,并将 heal()
方法添加到 Healer
。
字符选择.js
... Warrior.prototype.attack = function () { return `${this.name} attacks with the ${this.weapon}.`; } Healer.prototype.heal = function () { return `${this.name} casts ${this.spell}.`; }
此时,我们将使用两个可用的新角色类创建我们的角色。
字符选择.js
const hero1 = new Warrior('Bjorn', 1, 'axe'); const hero2 = new Healer('Kanin', 1, 'cure');
hero1
现在被识别为具有新属性的 Warrior
。
OutputWarrior {name: "Bjorn", level: 1, weapon: "axe"}
我们可以使用我们在 Warrior
原型上设置的新方法。
hero1.attack();
Console"Bjorn attacks with the axe."
但是如果我们尝试在原型链的下游使用方法会发生什么?
hero1.greet();
OutputUncaught TypeError: hero1.greet is not a function
当您使用 call()
链接构造函数时,原型属性和方法不会自动链接。 我们将使用 Object.setPropertyOf()
将 Hero
构造函数中的属性链接到 Warrior
和 Healer
构造函数,确保将其放在任何其他方法之前。
字符选择.js
... Object.setPrototypeOf(Warrior.prototype, Hero.prototype); Object.setPrototypeOf(Healer.prototype, Hero.prototype); // All other prototype methods added below ...
现在我们可以成功地在 Warrior
或 Healer
的实例上使用来自 Hero
的原型方法。
hero1.greet();
Output"Bjorn says hello."
这是我们的角色创建页面的完整代码。
字符选择.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 Object.setPrototypeOf(Warrior.prototype, Hero.prototype); Object.setPrototypeOf(Healer.prototype, 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
构造函数,从原始构造函数创建了两个名为 Warrior
和 Healer
的字符构造函数,将方法添加到原型并创建单个字符实例。
结论
JavaScript 是一种基于原型的语言,其功能不同于许多其他面向对象语言使用的传统的基于类的范例。
在本教程中,我们了解了原型如何在 JavaScript 中工作,以及如何通过所有对象共享的隐藏 Prototype
属性链接对象属性和方法。 我们还学习了如何创建自定义构造函数以及原型继承如何传递属性和方法值。