JavaScript 继承和原型链
栏目: JavaScript · 发布时间: 5年前
内容简介:小编推荐:
小编推荐: 掘金是一个面向 程序员 的高质量技术社区,从 一线大厂经验分享到前端开发最佳实践,无论是入门还是进阶,来掘金你不会错过前端开发的任何一个技术干货。
本文为 JavaScript Class(类) 中的 Private(私有) 和 Public(公有) 属性 续篇
之前我们学习了如何在 ES5 和 ES6 中创建 Animal
类。我们还学习了如何使用 JavaScrip t的原型在这些类之间共享方法。查看我们在之前文章中看到的代码。
ES5:
function Animal (name, energy) { this.name = name this.energy = energy } Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount } Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length } Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length } const leo = new Animal('Leo', 7)
ES6:
class Animal { constructor(name, energy) { this.name = name this.energy = energy } eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount } sleep() { console.log(`${this.name} is sleeping.`) this.energy += length } play() { console.log(`${this.name} is playing.`) this.energy -= length } } const leo = new Animal('Leo', 7)
现在我们想为特定动物建一个别 class(类) 。 例如,如果我们想要开始制作一堆狗实例,该怎么办? 这些狗有哪些属性和方法? 嗯,类似于我们的 Animal
类,我们可以给每只狗一个 name
,一个 energy
等级,以及 eat
, sleep
和 play
的能力。 我们的 Dog
类是独一无二的,我们也可以给 Dog
类一些独一无二的的属性,比如一个 breed
(品种) 属性以及 bark
(吠叫) 的能力。 在 ES5 中,我们的 Dog
类可能看起来像这样:
function Dog (name, energy, breed) { this.name = name this.energy = energy this.breed = breed } Dog.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount } Dog.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length } Dog.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length } Dog.prototype.bark = function () { console.log('Woof-Woof!') this.energy -= .1 } const charlie = new Dog('Charlie', 10, 'Goldendoodle')
你应该看出来了,我们刚刚重新创建了 Animal
类并为它添加了一些新属性。 如果我们想创建另一个动物,比如说 Cat
,那么我们必须再次创建一个 Cat
类,将 Animal
类中的所有常用逻辑复制到 Cat
,然后像 Dog
类一样添加 Cat
特定属性。 就是说,我们必须对我们创造的每一种不同类型的动物都这样做。
function Dog (name, energy, breed) {} function Cat (name, energy, declawed) {} function Giraffe (name, energy, height) {} function Monkey (name, energy, domesticated) {}
这项工作似乎很浪费。 Animal
类是完美的基类。 这意味着它具有我们每只动物的共同特征。 无论我们是创造 狗,猫,长颈鹿还是猴子,它们都会有一个 name
, energy
等级,以及 eat
, sleep
和 play
的能力。 那么每当我们为每个不同的动物创建单独的类时,我们是否可以利用 Animal
类? 我们来试试吧。 我将在下面再次粘贴 Animal
类以便于参考。
function Animal (name, energy) { this.name = name this.energy = energy } Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount } Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length } Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length } function Dog (name, energy, breed) { }
我们对上面的 Dog
构造函数你了解多少?
首先,我们知道它需要3个参数, name
, energy
和 breed
。
其次,我们知道它将使用 new
关键字调用,因此我们将拥有一个 this
对象。
第三,我们知道我们需要利用 Animal
函数,这样任何狗的实例都会有一个 name
, energy
等级,以及 eat
, sleep
和 play
的能力。
第三点是有点棘手的问题。 你“利用”一个函数的方式就是调用它。 所以我们知道在 Dog
里面,我们想要调用 Animal
。 我们需要弄清楚的是我们如何在 Dog
的上下文中调用 Animal
。 这意味着我们想用 Dog
中的 this
关键字调用 Animal
。 如果我们正确地做到了,那么 Dog
函数内部将具有 Animal
的所有属性( name
, energy
)。 如果您记得 上一节我们所讨论的内容 ,JavaScript 中的每个函数都有一个 .call
方法。
.call()
是函数的一个方法,它允许您调用函数时,指定该函数的上下文。
听起来正是我们所需要的。我们想在 Dog
上下文中调用 Animal
。
function Dog (name, energy, breed) { Animal.call(this, name, energy) this.breed = breed } const charlie = new Dog('Charlie', 10, 'Goldendoodle') charlie.name // Charlie charlie.energy // 10 charlie.breed // Goldendoodle
知道这个,我们就已经成功一半了。您将在上面的代码中注意到,因为这一行是 Animal.call(this, name, energy)
, Dog
的每个实例都将有一个 name
和 energy
属性。同样,这样做的原因是,就好像我们使用从 Dog
生成的 this
关键字运行 Animal
函数一样。在我们添加了一个 name
和 energy
属性之后,我们又像往常一样添加了一个 breed
属性。
请记住,这里的目标是让 Dog
的每个实例不仅具有 Animal
的所有属性,而且还具有所有方法。如果您运行上面的代码,您会注意到如果您尝试运行 charlie.eat(10)
,您将收到一个错误。目前 Dog
的每个实例都具有 Animal
( name
和 energy
)的属性,但我们没有做任何事情来确保他们也有方法( eat
, sleep
和 play
)。
让我们考虑如何解决这个问题。我们知道所有 Animal
的方法都位于 Animal.prototype
上。这意味着我们想要确保 Dog
的所有实例都可以访问 Animal.prototype
上的方法。如果我们在这里使用我们的好朋友 Object.create
怎么办?如果您还记得, Object.create
允许您创建一个对象,该对象将在失败的查找中委托给另一个对象。所以在我们的例子中,我们想要创建的对象将是 Dog
的原型,而我们想要在失败的查找中委托的对象是 Animal.prototype
。
function Dog (name, energy, breed) { Animal.call(this, name, energy) this.breed = breed } Dog.prototype = Object.create(Animal.prototype)
现在,只要在 Dog
实例上查找失败,JavaScript 就会将该查找委托给 Animal.prototype
。 如果这仍然有点模糊,请重新阅读 JavaScript Prototype(原型) 新手指南 ,其中我们讨论了 Object.create
和 JavaScript 的 原型(prototype) 。
让我们一起看完整个代码,然后我们将了解发生的事情。
function Animal (name, energy) { this.name = name this.energy = energy } Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount } Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length } Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length } function Dog (name, energy, breed) { Animal.call(this, name, energy) this.breed = breed } Dog.prototype = Object.create(Animal.prototype)
现在我们已经创建了我们的基类( Animal
)以及我们的子类( Dog
),让我们在创建 Dog
实例时看看它的样子。
const charlie = new Dog('Charlie', 10, 'Goldendoodle') charlie.name // Charlie charlie.energy // 10 charlie.breed // Goldendoodle
到目前为止没有任何花哨的东西,但让我们来看看当我们调用位于 Animal
上的方法时会发生什么。
charlie.eat(10) /* 1) JavaScript checks if charlie has an eat property - it doesn't. 2) JavaScript then checks if Dog.prototype has an eat property - it doesn't. 3) JavaScript then checks if Animal.prototype has an eat property - it does so it calls it. */
Dog.prototype
被检查的原因是因为当我们创建一个新的 Dog
实例时,我们使用了 new
关键字。在引擎中,为我们创建的 this
对象委托给 Dog.prototype
(见下面的注释)。
function Dog (name, energy, breed) { // this = Object.create(Dog.prototype) Animal.call(this, name, energy) this.breed = breed // return this }
之所以检查 Animal.prototype
是因为我们用这一行覆盖了 Dog.prototype
以委托给失败的查找的 Animal.prototype
Dog.prototype = Object.create(Animal.prototype)
现在我们还没有谈到的一件事是,如果 Dog
有自己的方法呢? 嗯,这是一个简单的解决方案。 就像 Animal
一样,如果我们想在该类的所有实例之间共享一个方法,我们将它添加到函数的原型中。
... function Dog (name, energy, breed) { Animal.call(this, name, energy) this.breed = breed } Dog.prototype = Object.create(Animal.prototype) Dog.prototype.bark = function () { console.log('Woof Woof!') this.energy -= .1 }
非常好。我们需要做一个小小的补充。如果你不记得了请回到 JavaScript Prototype(原型) 新手指南 了解详情,我们可以通过使用 instance.constructor
来访问实例的构造函数。
function Animal (name, energy) { this.name = name this.energy = energy } const leo = new Animal('Leo', 7) console.log(leo.constructor) // Logs the constructor function
正如前一篇文章中所解释的那样,“其工作原因是因为任何 Animal
实例都会在失败的查找中委托给 Animal.prototype
。 因此,当您尝试访问 leo.prototype
时, leo
没有 prototype
属性,因此它会将该查找委托给 Animal.prototype
,它确实具有 constructor
属性。“
我提出这个问题的原因是因为在我们的实现中,我们用一个委托给 Animal.prototype
的对象覆盖了 Dog.prototype
。
function Dog (name, energy, breed) { Animal.call(this, name, energy) this.breed = breed } Dog.prototype = Object.create(Animal.prototype) Dog.prototype.bark = function () { console.log('Woof Woof!') this.energy -= .1 }
这意味着现在,任何打印 Dog
的实例 instance.constructor
都将获得 Animal
构造函数而不是 Dog
构造函数。您可以通过运行此代码自行查看 –
function Animal (name, energy) { this.name = name this.energy = energy } Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount } Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length } Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length } function Dog (name, energy, breed) { Animal.call(this, name, energy) this.breed = breed } Dog.prototype = Object.create(Animal.prototype) Dog.prototype.bark = function () { console.log('Woof Woof!') this.energy -= .1 } const charlie = new Dog('Charlie', 10, 'Goldendoodle') console.log(charlie.constructor)
请注意,即使 charlie
是 Dog
的直接实例,它也会为您提供 Animal
构造函数。同样,我们可以像上面一样了解这里发生的事情。
const charlie = new Dog('Charlie', 10, 'Goldendoodle') console.log(charlie.constructor) /* 1) JavaScript checks if charlie has a constructor property - it doesn't. 2) JavaScript then checks if Dog.prototype has a constructor property - it doesn't because it was deleted when we overwrote Dog.prototype. 3) JavaScript then checks if Animal.prototype has a constructor property - it does so it logs that. */
我们该如何解决这个问题?嗯,这很简单。一旦我们覆盖它,我们就可以向 Dog.prototype
添加正确的 constructor
属性。
function Dog (name, energy, breed) { Animal.call(this, name, energy) this.breed = breed } Dog.prototype = Object.create(Animal.prototype) Dog.prototype.bark = function () { console.log('Woof Woof!') this.energy -= .1 } Dog.prototype.constructor = Dog
此时如果我们想要创建另一个子类,比如 Cat
,我们将遵循相同的模式。
function Cat (name, energy, declawed) { Animal.call(this, name, energy) this.declawed = declawed } Cat.prototype = Object.create(Animal.prototype) Cat.prototype.constructor = Cat Cat.prototype.meow = function () { console.log('Meow!') this.energy -= .1 }
这种具有委托给它的子类的基类的概念称为继承,它是面向对象编程(OOP)的主要部分。 如果你来自不同的编程语言,你可能已经熟悉OOP和继承了。 在 ES6 classes 之前,在 JavaScript 中,继承是一项非常艰巨的任务,正如您在上面所看到的。您现在只需要了解什么时候使用继承,以及 .call
和 Object.create
, this
,和 FN.prototype
的良好组合。- 这些都是高级 JS 主题。让我们看看如何使用 ES6 类来完成同样的事情。
首先,让我们回顾一下使用我们的 Animal
类从 ES5 “类” 到 ES6 类的样子。
ES5:
function Animal (name, energy) { this.name = name this.energy = energy } Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount } Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length } Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length } const leo = new Animal('Leo', 7)
ES6:
class Animal { constructor(name, energy) { this.name = name this.energy = energy } eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount } sleep() { console.log(`${this.name} is sleeping.`) this.energy += length } play() { console.log(`${this.name} is playing.`) this.energy -= length } } const leo = new Animal('Leo', 7)
现在我们已经将我们的 Animal
构造函数重构为 ES6 类,接下来我们需要做的是弄清楚如何重构我们的基类( Dog
)。好消息是它更加直观。作为参考,在ES5 中,这是我们所拥有的。
function Dog (name, energy, breed) { Animal.call(this, name, energy) this.breed = breed } Dog.prototype = Object.create(Animal.prototype) Dog.prototype.bark = function () { console.log('Woof Woof!') this.energy -= .1 } Dog.prototype.constructor = Dog
在我们进入继承之前,让我们使用 ES6 类来重构 Dog
,就像我们在之前的帖子中学到的那样。
class Dog { constructor(name, energy, breed) { this.breed = breed } bark() { console.log('Woof Woof!') this.energy -= .1 } }
看起来很棒。现在,让我们弄清楚如何确保 Dog
继承自 Animal
。我们需要做的第一步是非常直接的。使用 ES6 类,您可以使用此语法 extend
基类
class Subclass extends Baseclass {}
翻译成我们的例子,这将使我们的 Dog
类看起来像这样:
class Animal { constructor(name, energy) { this.name = name this.energy = energy } eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount } sleep() { console.log(`${this.name} is sleeping.`) this.energy += length } play() { console.log(`${this.name} is playing.`) this.energy -= length } } class Dog extends Animal { constructor(name, energy, breed) { this.breed = breed } bark() { console.log('Woof Woof!') this.energy -= .1 } }
在ES5中,为了确保 Dog
的每个实例都具有 name
和 energy
属性,我们使用 .call
以在 Dog
实例的上下文中调用 Animal
构造函数。 幸运的是,在 ES6 中,它更直接。 每当你扩展一个基类并且你需要调用那个基类的构造函数时,你调用 super
传递它需要的任何参数即可。 所以在我们的例子中,我们的 Dog
构造函数被重构为这样:
class Animal { constructor(name, energy) { this.name = name this.energy = energy } eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount } sleep() { console.log(`${this.name} is sleeping.`) this.energy += length } play() { console.log(`${this.name} is playing.`) this.energy -= length } } class Dog extends Animal { constructor(name, energy, breed) { super(name, energy) // calls Animal's constructor this.breed = breed } bark() { console.log('Woof Woof!') this.energy -= .1 } }
就是这样。不使用 .call
,不使用 Object.create
,不用担心重置原型上的构造函数 – 只需 extends
基类并确保调用 super
即可。
JavaScript 的有趣之处在于您学到的相同模式,最后几篇文章直接融入语言本身。 以前您了解到 Array
的所有实例都可以访问 pop
, slice
, filter
等数组方法的原因是因为所有这些方法都存在于 Array.prototype
中。
console.log(Array.prototype) /* concat: ƒn concat() constructor: ƒn Array() copyWithin: ƒn copyWithin() entries: ƒn entries() every: ƒn every() fill: ƒn fill() filter: ƒn filter() find: ƒn find() findIndex: ƒn findIndex() forEach: ƒn forEach() includes: ƒn includes() indexOf: ƒn indexOf() join: ƒn join() keys: ƒn keys() lastIndexOf: ƒn lastIndexOf() length: 0n map: ƒn map() pop: ƒn pop() push: ƒn push() reduce: ƒn reduce() reduceRight: ƒn reduceRight() reverse: ƒn reverse() shift: ƒn shift() slice: ƒn slice() some: ƒn some() sort: ƒn sort() splice: ƒn splice() toLocaleString: ƒn toLocaleString() toString: ƒn toString() unshift: ƒn unshift() values: ƒn values() */
您知道,所有 Object
实例都可以访问 hasOwnProperty
和 toString
等方法的原因是因为这些方法存在于 Object.prototype
上。
console.log(Object.prototype) /* constructor: ƒn Object() hasOwnProperty: ƒn hasOwnProperty() isPrototypeOf: ƒn isPrototypeOf() propertyIsEnumerable: ƒn propertyIsEnumerable() toLocaleString: ƒn toLocaleString() toString: ƒn toString() valueOf: ƒn valueOf() */
这对你来说是一个挑战。使用上面的 Array 方法和 Object 方法列表,为什么下面的代码有效?
const friends = ['Mikenzi', 'Jake', 'Ean'] friends.hasOwnProperty('push') // false
如果查看 Array.prototype
,则没有 hasOwnProperty
方法。 好吧,如果 Array.prototype
上没有 hasOwnProperty
方法,上面示例中的 friends
数组如何访问 hasOwnProperty
? 原因是因为 Array
类扩展了 Object
类。 因此,在上面的示例中,当 JavaScript 看到 friends
没有 hasOwnProperty
属性时,它会检查 Array.prototype
是否具有该方法。 当 Array.prototype
没有时,它会检查 Object.prototype
是否有该方法,然后再调用它。 这是我们在这篇博客文章中看到的相同过程。
JavaScript 有两种类型 – 原始类型 和 引用类型 。
原始类型是 boolean
, number
, string
, null
和 undefined
并且是不可变的。 其他所有内容都是引用类型,它们都扩展了 Object.prototype
。 这就是为什么你可以为函数和数组添加属性,这就是为什么函数和数组都可以访问 Object.prototype
上的方法。
function speak(){} speak.woahFunctionsAreLikeObjects = true console.log(speak.woahFunctionsAreLikeObjects) // true const friends = ['Mikenzi', 'Jake', 'Ean'] friends.woahArraysAreLikeObjectsToo = true console.log(friends.woahArraysAreLikeObjectsToo) // true
英文原文:https://tylermcginnis.com/javascript-inheritance-and-the-prototype-chain/
如果你觉得本文对你有帮助,那就请分享给更多的朋友
关注「前端干货精选」加星星,每天都能获取前端干货
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 构造函数、原型、原型链、继承
- 前端基本功(七):javascript中的继承(原型、原型链、继承的实现方式)
- JavaScript原型链继承
- 彻底弄懂JS原型与继承
- 前端进击的巨人(七):走进面向对象,原型与原型链,继承方式
- 从原型聊到原型继承,深入理解 JavaScript 面向对象精髓
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
C语言的科学和艺术
罗伯茨 / 翁惠玉 / 机械工业出版社 / 2005-3 / 55.00元
《C语言的科学和艺术》是计算机科学的经典教材,介绍了计算机科学的基础知识和程序设计的专门知识。《C语言的科学和艺术》以介绍ANSI C为主线,不仅涵盖C语言的基本知识,而且介绍了软件工程技术以及如何应用良好的程序设计风格进行开发等内容。《C语言的科学和艺术》采用了库函数的方法,强调抽象的原则,详细阐述了库和模块化开发。此外,《C语言的科学和艺术》还利用大量实例讲述解决问题的全过程,对开发过程中常见......一起来看看 《C语言的科学和艺术》 这本书的介绍吧!
HTML 压缩/解压工具
在线压缩/解压 HTML 代码
HEX HSV 转换工具
HEX HSV 互换工具