JavaScript 继承和原型链

栏目: JavaScript · 发布时间: 5年前

内容简介:小编推荐:

JavaScript 继承和原型链

JavaScript 继承和原型链

小编推荐: 掘金是一个面向 程序员 的高质量技术社区,从 一线大厂经验分享到前端开发最佳实践,无论是入门还是进阶,来掘金你不会错过前端开发的任何一个技术干货。

本文为 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 等级,以及 eatsleepplay 的能力。 我们的 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 类是完美的基类。 这意味着它具有我们每只动物的共同特征。 无论我们是创造 狗,猫,长颈鹿还是猴子,它们都会有一个 nameenergy 等级,以及 eatsleepplay 的能力。 那么每当我们为每个不同的动物创建单独的类时,我们是否可以利用 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 , energybreed

其次,我们知道它将使用 new 关键字调用,因此我们将拥有一个 this 对象。

第三,我们知道我们需要利用 Animal 函数,这样任何狗的实例都会有一个 nameenergy 等级,以及 eatsleepplay 的能力。

第三点是有点棘手的问题。 你“利用”一个函数的方式就是调用它。 所以我们知道在 Dog 里面,我们想要调用 Animal 。 我们需要弄清楚的是我们如何在 Dog 的上下文中调用 Animal 。 这意味着我们想用 Dog 中的 this 关键字调用 Animal 。 如果我们正确地做到了,那么 Dog 函数内部将具有 Animal 的所有属性( nameenergy )。 如果您记得 上一节我们所讨论的内容 ,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 的每个实例都将有一个 nameenergy 属性。同样,这样做的原因是,就好像我们使用从 Dog 生成的 this 关键字运行 Animal 函数一样。在我们添加了一个 nameenergy 属性之后,我们又像往常一样添加了一个 breed 属性。

请记住,这里的目标是让 Dog 的每个实例不仅具有 Animal 的所有属性,而且还具有所有方法。如果您运行上面的代码,您会注意到如果您尝试运行 charlie.eat(10) ,您将收到一个错误。目前 Dog 的每个实例都具有 Animalnameenergy )的属性,但我们没有做任何事情来确保他们也有方法( eatsleepplay )。

让我们考虑如何解决这个问题。我们知道所有 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)

请注意,即使 charlieDog 的直接实例,它也会为您提供 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 中,继承是一项非常艰巨的任务,正如您在上面所看到的。您现在只需要了解什么时候使用继承,以及 .callObject.createthis ,和 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 的每个实例都具有 nameenergy 属性,我们使用 .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 的所有实例都可以访问 popslicefilter 等数组方法的原因是因为所有这些方法都存在于 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 实例都可以访问 hasOwnPropertytoString 等方法的原因是因为这些方法存在于 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 有两种类型 – 原始类型 和 引用类型 。

原始类型是 booleannumberstringnullundefined 并且是不可变的。 其他所有内容都是引用类型,它们都扩展了 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 继承和原型链

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

C语言的科学和艺术

C语言的科学和艺术

罗伯茨 / 翁惠玉 / 机械工业出版社 / 2005-3 / 55.00元

《C语言的科学和艺术》是计算机科学的经典教材,介绍了计算机科学的基础知识和程序设计的专门知识。《C语言的科学和艺术》以介绍ANSI C为主线,不仅涵盖C语言的基本知识,而且介绍了软件工程技术以及如何应用良好的程序设计风格进行开发等内容。《C语言的科学和艺术》采用了库函数的方法,强调抽象的原则,详细阐述了库和模块化开发。此外,《C语言的科学和艺术》还利用大量实例讲述解决问题的全过程,对开发过程中常见......一起来看看 《C语言的科学和艺术》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具