JavaScript Prototype(原型) 新手指南---由浅入深

89 阅读15分钟
原文链接: www.css88.com
10年服务1亿前端开发工程师

引言:如果不处理对象,您就无法在 JavaScript 方面取得很大进展。它们几乎是 JavaScript 编程语言的所有方面的基础。在这篇文章中,您将了解用于实例化新对象的各种模式,在此过程中,您将逐步深入了解 JavaScript 的原型。

基础入门

如果不处理对象,您就无法在 JavaScript 方面取得很大进展。它们几乎是 JavaScript 编程语言的所有方面的基础。事实上,学习如何创建对象可能是你刚开始学习的第一件事。话虽如此,为了最有效地学习 JavaScript 中的原型,我们将从基础开始。

首先,对象是键/值对。创建对象的最常用方法是使用花括号{},并使用点表示法向对象添加属性和方法。

这个很简单。现在,我们在应用程序中我们需要创建多个动物。很自然地,下一步就是将逻辑封装到一个函数中,以便我们在需要创建新动物时调用这个函数。我们将调用这个模式 Functional Instantiation(函数实例化),并将函数本身称为 constructor function(构造函数) ,因为它负责“构造”一个新对象。

Functional Instantiation (函数实例化)

function Animal (name, energy) {
  let animal = {}
  animal.name = name
  animal.energy = energy

  animal.eat = function (amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }

  animal.sleep = function (length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }

  animal.play = function (length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }

  return animal
}

const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)

现在,每当我们想要创建一种新动物(或者更广泛地说是一种新的“实例”)时,我们所要做的就是调用我们的 Animal 函数,将动物的 nameenergy 传递给这个函数。这非常有效,而且非常简单。但是,你有发现这种模式的不足之处吗?我们要尝试解决的最大的问题与三种方法有关 – eatsleepplay。这些方法中的每一种都不仅是动态的,而且它们也是完全通用的。这意味着没有理由重新创建这些方法,正如我们在创建新动物时所做的那样。我们只是在浪费内存,让每个动物物体都比它需要的更大。你能想到一个解决方案吗?
如果我们每次创建一个新动物时不需要重新创建这些方法,而是将它们移动到它们自己的对象上,那么我们就可以让每个动物引用那个对象了?我们可以把这种模式称为 Functional Instantiation with Shared Methods(共享方法的函数实例化) 。描述起来有点啰嗦 🤷‍ 。

Functional Instantiation with Shared Methods (共享方法的函数实例化)

const animalMethods = {
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  },
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  },
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

function Animal (name, energy) {
  let animal = {}
  animal.name = name
  animal.energy = energy
  animal.eat = animalMethods.eat
  animal.sleep = animalMethods.sleep
  animal.play = animalMethods.play

  return animal
}

const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)

通过将共享方法移动到它们自己的对象并在 Animal 函数中引用该对象,我们现在已经解决了内存浪费和动物对象过大的问题。

Object.create

让我们使用 Object.create 再次改进我们的例子。 简而言之,Object.create 允许您创建一个对象,该对象将在查找失败时委托给另一个对象。 换句话说,Object.create 允许您创建一个对象,只要该对象上的属性查找失败,它就可以查询另一个对象,以查看另一个对象中是否具有该属性。 说清楚需要很多文字, 我们来看一些代码。

const parent = {
  name: 'Stacey',
  age: 35,
  heritage: 'Irish'
}

const child = Object.create(parent)
child.name = 'Ryan'
child.age = 7

console.log(child.name) // Ryan
console.log(child.age) // 7
console.log(child.heritage) // Irish

在上面的示例中,因为 child 是通过 Object.create(parent) 创建的,所以每当在 child 中查找属性失败时,JavaScript 就会将该查找委托给 parent 对象。这意味着即使 child 没有 heritage 属性,当你查找 child.heritage 时你会得到 parentheritage 属性,即 Irish

现在,通过使用 Object.create ,我们该如何使用它来简化之前的 Animal 代码呢?好吧,我们可以使用 Object.create 委托给animalMethods 对象,而不是像我们之前一样逐个将所有共享方法添加到 Animal 中。 为了听起来很智能,让我们称之为 Functional Instantiation with Shared Methods and Object.create(使用共享方法和Object.create进行函数实例化) 🙃 。

Functional Instantiation with Shared Methods and Object.create (使用共享方法和Object.create进行函数实例化)

const animalMethods = {
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  },
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  },
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

function Animal (name, energy) {
  let animal = Object.create(animalMethods)
  animal.name = name
  animal.energy = energy

  return animal
}

const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)

leo.eat(10)
snoop.play(5)

所以现在当我们调用 leo.eat 时,JavaScript 会在 leo 对象上查找 eat 方法。 这个查找将失败,因为使用了 Object.create,它将委托给 animalMethods 对象,然后在这里将找到 eat 方法。

到现在为止还挺好的。 尽管如此,我们仍然可以做出一些改进。 为了跨实例共享方法,必须管理一个单独的对象(animalMethods)似乎有点“hacky”。 这似乎是您希望在语言本身中实现的常见功能。 事实证明,这就是你看这篇文章的原因 – prototype(原型) 。

那么究竟什么是 JavaScript 的 prototype(原型)呢? 简单地说,JavaScript 中的每个函数都有一个引用对象的 prototype 属性。 我们来亲自测试一下。

function doThing () {}
console.log(doThing.prototype) // {}

如果不是创建一个单独的对象(比如我们正在使用的 animalMethods )来管理我们的方法,也就是我们只是将每个方法放在 Animal 函数的 prototype(原型) 对象上,该怎么办呢?我们所要做的就是不使用 Object.create 委托给 animalMethods,我们可以用使用来委托Animal.prototype。 我们将这种模式称为 Prototypal Instantiation(原型实例化)。

Prototypal Instantiation (原型实例化)

function Animal (name, energy) {
  let animal = Object.create(Animal.prototype)
  animal.name = name
  animal.energy = energy

  return animal
}

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 = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)

leo.eat(10)
snoop.play(5)

👏👏👏 这里你可以为自己鼓掌鼓励一下了。 同样,原型只是 JavaScript 中每个函数都具有的属性,并且如上所述,它允许我们在函数的所有实例之间共享方法。 我们所有的功能仍然相同,但现在我们不必为所有方法管理一个单独的对象,我们可以使用另一个内置于 Animal 函数本身的对象Animal.prototype


更深的,走起!

首先,我们需要知道三件事:

  1. 如何创建构造函数。
  2. 如何将方法添加到构造函数的原型中。
  3. 如何使用 Object.create 将失败的查找委托给函数的原型。

这三个任务似乎是任何编程语言的基础。 JavaScript 是否真的那么糟糕,没有更简单,“内置”的方式来完成同样的事情? 正如你可能已经猜测的那样,它是通过使用 new 关键字。

我们采用的这种缓慢而有条理的方法的好处是,您现在可以深入了解 JavaScript 中的 new 关键字在幕后的作用。

回顾一下我们的 Animal 构造函数,最重要的两个部分是创建对象并返回它。 如果不使用 Object.create 创建对象,我们将无法在查找失败时委托给函数的原型。 如果没有 return 语句,我们将永远不会返回创建的对象。

function Animal (name, energy) {
  let animal = Object.create(Animal.prototype)
  animal.name = name
  animal.energy = energy

  return animal
}

new 有一个很酷的地方——当您使用 new 关键字调用函数时,注释掉的这两行代码是隐式(引擎)完成的,创建的对象称为 this

使用注释来显示在幕后发生的事情并假设使用 new 关键字调用 Animal 构造函数,可以将其重写为这样:

function Animal (name, energy) {
  // const this = Object.create(Animal.prototype)

  this.name = name
  this.energy = energy

  // return this
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

去掉注释后:

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)
const snoop = new Animal('Snoop', 10)

同样,这样做以及为我们创建 this 对象的原因是,我们使用 new 关键字调用构造函数。如果在调用函数时不使用 new ,则该对象永远不会创建,也不会隐式返回。我们可以在下面的例子中看到这个问题。

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

const leo = Animal('Leo', 7)
console.log(leo) // undefined

此模式的名称是 Pseudoclassical Instantiation(伪类实例化) 。

如果 JavaScript 不是您的第一种编程语言,您可能会有点不安。

“WTF这个家伙只是重新创造了一个更糟糕的版本” – 你

对于那些不熟悉的人,Class(类) 允许您为对象创建模板。然后,无论何时创建该类的实例,都会获得一个具有模板中定义的属性和方法的对象。

听起来有点熟悉?这基本上就是我们对上面的 Animal 构造函数所做的事情。但是对于 Animal 构造函数,我们只使用常规的旧 JavaScript 函数来重新创建相同的功能,而不是使用 class 关键字。当然,它需要一些额外的工作以及一些关于 JavaScript “引擎” 所处理的事情的相关知识,但结果是一样的。

这是个好消息。 JavaScript 不是一种 “死” 语言。它不断得到改进,并由 TC-39委员会 不断的制定标准。这意味着即使 JavaScript 的初始版本不支持类,也不影响后续将它们添加到官方规范中。事实上,这正是TC-39委员会所做的事情。 2015年,发布了 EcmaScript(官方JavaScript规范)6 ,支持 Classes(类) 和 class 关键字。让我们看看上面的 Animal 构造函数如何使用新的 class(类) 语法。

class Animal {
  constructor(name, energy) {
    this.name = name
    this.energy = energy
  }
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

很干净是吧?

因此,如果这是创建类的新方法,为什么我们前面花了这么多时间来讨论旧的方式呢? 原因是新的方式(使用 class 关键字)只是经典伪类模式的 “语法糖”。 为了完全理解 ES6 类的便捷语法,首先必须理解经典的伪类模式。

关于 ES6 Classes(类) 更多相关信息可以查看 面向对象的 JavaScript – 深入了解 ES6 类


我们已经介绍了 JavaScript 原型的基础知识。 本文的其余部分将致力于加深理解相关知识的主题。 在另一篇文章中,我们将看看如何利用这些基础知识并使用它们来理解 JavaScript 中继承的工作原理。


数组方法

我们在上面深入讨论了如何在类的实例之间共享方法,您应该将这些方法放在类(或函数)原型上。 如果我们查看 Array 类,我们可以看到相同的模式。 从历史上看,您可能已经创建了这样的数组:

const friends = []

事实证明,这只是创建一个新的 Array 类实例的语法糖。

const friendsWithSugar = []

const friendsWithoutSugar = new Array()

您可能从未想过:数组的每个实例是如何具有所有内置方法的(splice , slice, pop 等)?

正如您现在所知,这是因为这些方法存在于 Array.prototype 上,当您创建一个新的 Array 实例时,您使用 new 关键字在查找失败时将该委托设置为 Array.prototype

我们可以通过简单地 console.log(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()
*/

Objects(对象) 也是完全相同的逻辑。 所有对象将在查找失败时委托给 Object.prototype ,这就是所有对象都有 toStringhasOwnProperty 等方法的原因。

静态方法

到目前为止,我们已经介绍了为什么,以及如何在类的实例之间共享方法。 但是,如果我们有一个对 Class 很重要但不需要又跨实例共享的方法,该怎么办呢? 例如,如果我们有一个函数,它接收一系列 Animal 实例并决定下一个需要喂食的对象,会怎样? 我们将其称为 nextToEat

function nextToEat (animals) {
  const sortedByLeastEnergy = animals.sort((a,b) => {
    return a.energy - b.energy
  })

  return sortedByLeastEnergy[0].name
}

我们不希望在所有实例之间共享它,所以在 Animal.prototype 上使用 nextToEat 是没有意义的。 相反,我们可以将其视为辅助方法。 所以如果 nextToEat 不应该存在于 Animal.prototype 中,我们应该把它放在哪里呢? 那么显而易见的答案是我们可以将 nextToEat 放在与 Animal 类相同的作用域中,然后像我们平常那样,在需要时引用它。

class Animal {
  constructor(name, energy) {
    this.name = name
    this.energy = energy
  }
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

function nextToEat (animals) {
  const sortedByLeastEnergy = animals.sort((a,b) => {
    return a.energy - b.energy
  })

  return sortedByLeastEnergy[0].name
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

console.log(nextToEat([leo, snoop])) // Leo

现在这可行,但有更好的方法。

只要有一个特定于类本身的方法,但不需要在该类的实例之间共享,就可以将其添加为类的 static(静态) 属性。

class Animal {
  constructor(name, energy) {
    this.name = name
    this.energy = energy
  }
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
  static nextToEat(animals) {
    const sortedByLeastEnergy = animals.sort((a,b) => {
      return a.energy - b.energy
    })

    return sortedByLeastEnergy[0].name
  }
}

现在,因为我们在类上添加了 nextToEat 作为 static(静态) 属性,所以它存在于 Animal 类本身(而不是它的原型)中,并且可以使用 Animal.nextToEat 进行访问。

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

console.log(Animal.nextToEat([leo, snoop])) // Leo

因为我们在这篇文章中都遵循了类似的模式,让我们来看看如何使用 ES5 完成同样的事情。 在上面的例子中,我们看到了如何使用 static 关键字将方法直接放在类本身上。 使用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
}

Animal.nextToEat = function (nextToEat) {
  const sortedByLeastEnergy = animals.sort((a,b) => {
    return a.energy - b.energy
  })

  return sortedByLeastEnergy[0].name
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

console.log(Animal.nextToEat([leo, snoop])) // Leo

获取对象的原型

无论您使用哪种模式创建对象,都可以使用 Object.getPrototypeOf 方法完成获取该对象的原型。

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)
const prototype = Object.getPrototypeOf(leo)

console.log(prototype)
// {constructor: ?, eat: ?, sleep: ?, play: ?}

prototype === Animal.prototype // true

上面的代码有两个要点。

首先,你会注意到 proto 是一个对象,有4种方法,constructoreatsleep,和play。那讲得通。我们将实例传递给getPrototypeOfleo 获取了实例的原型,这里是所有的方法。这提示我们,关于原型的另外一件事我们还没有讨论过。默认情况下,原型对象将具有 constructor 属性,该属性指向原始函数或创建实例的类。这也意味着 JavaScript 默认在原型上放置 constructor 属性,所以任何实例都可以通过 instance.constructor 访问它们的构造函数。

上面的第二个要点是 Object.getPrototypeOf(leo) === Animal.prototype 。这也是有道理的。 Animal 构造函数有一个 prototype(原型) 属性,我们可以在所有实例之间共享方法,getPrototypeOf 允许我们查看实例本身的原型。

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

const leo = new Animal('Leo', 7)
console.log(leo.constructor) // Logs the constructor function

为了配合我们之前使用 Object.create 所讨论的内容,其工作原因是因为任何 Animal 实例都会在查找失败时委托给 Animal.prototype。 因此,当您尝试访问 leo.prototype 时, leo 没有 prototype 属性,因此它会将该查找委托给Animal.prototype,它确实具有 constructor 属性。 如果这段没有看懂,请回过头来阅读上面的 Object.create

您可能以前看到过使用 __proto__ 获取实例的原型。 这是过去的遗物。 现在,如上所述使用 Object.getPrototypeOf(instance) 获取实例的原型。

确定属性是否存在于原型上

在某些情况下,您需要知道属性是否存在于实例本身上,还是存在于对象委托的原型上。 我们可以通过循环我们创建的 leo 对象来知道这一点。假设目标是循环 leo 并记录其所有键和值。使用 for in 循环,可能看起来像这样。

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)

for(let key in leo) {
  console.log(`Key: ${key}. Value: ${leo[key]}`)
}

你期望看到什么?最有可能的是,它是这样的 –

Key: name. Value: Leo
Key: energy. Value: 7

但是,如果你运行代码,你看到的是这样的 –

Key: name. Value: Leo
Key: energy. Value: 7
Key: eat. Value: function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}
Key: sleep. Value: function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}
Key: play. Value: function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

这是为什么? for in 循环将循环遍历对象本身以及它所委托的原型的所有 可枚举属性 。 因为默认情况下,您添加到函数原型的任何属性都是可枚举的,我们不仅会看到nameenergy ,还会看到原型上的所有方法 – eatsleepplay 。 要解决这个问题,我们需要指定所有原型方法都是不可枚举的,或者如果属性在 leo 对象本身上而不是 leo 查找失败时委托给的原型上。 hasOwnProperty 可以帮助我们实现这个需求。

hasOwnProperty 是每个对象上的一个属性,它返回一个布尔值,指示对象是否具有指定的属性作为其自身的属性,而不是对象委托给的原型。 这正是我们所需要的。 现在有了这些新知识,我们可以修改我们的代码,以便利用 for in 循环中的 hasOwnProperty

...

const leo = new Animal('Leo', 7)

for(let key in leo) {
  if (leo.hasOwnProperty(key)) {
    console.log(`Key: ${key}. Value: ${leo[key]}`)
  }
}

而现在我们看到的只是 leo 对象本身的属性,而不是 leo 原型中的方法。

Key: name. Value: Leo
Key: energy. Value: 7

如果你仍然对 hasOwnProperty 感到困惑,这里有一些代码可以帮你消除困惑。

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)

leo.hasOwnProperty('name') // true
leo.hasOwnProperty('energy') // true
leo.hasOwnProperty('eat') // false
leo.hasOwnProperty('sleep') // false
leo.hasOwnProperty('play') // false

检查对象是否是类的实例

有时您想知道对象是否是指定类的实例。 为此,您可以使用 instanceof 运算符。 用例非常简单,但如果您以前从未见过它,实际的语法有点奇怪。 它的工作原理如下

object instanceof Class

如果 objectClass 的实例,则上面的语句将返回 true ,否则返回 false 。回到我们的 Animal 示例,我们会有类似的东西。

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

function User () {}

const leo = new Animal('Leo', 7)

leo instanceof Animal // true
leo instanceof User // false

instanceof 的工作方式是检查对象原型链中是否存在 constructor.prototype 。 在上面的例子中,leo instanceof Animaltrue ,因为Object.getPrototypeOf(leo) === Animal.prototype。 另外,leo instanceof Userfalse ,因为 Object.getPrototypeOf(leo) !== User.prototype

创建新的不可知构造函数

你能发现下面代码中的错误吗?

function Animal (name, energy) {
  this.name = name
  this.energy = energy
}

const leo = Animal('Leo', 7)

即使是经验丰富的 JavaScript 开发人员有时也会因为上面的例子而被绊倒。 因为我们正在使用之前学过的 pseudoclassical pattern(经典伪类模式),所以当调用 Animal 构造函数时,我们需要确保使用 new 关键字调用它。 如果我们不这样做,则不会创建 this 关键字,也不会隐式返回。

作为复习,注释掉的行是在函数上使用 new 关键字时幕后所做的事情。

function Animal (name, energy) {
  // const this = Object.create(Animal.prototype)

  this.name = name
  this.energy = energy

  // return this
}

这似乎是一个非常重要的细节,让其他开发人员记住。 假设我们正在与其他开发人员合作,有没有办法确保我们的 Animal 构造函数始终使用 new 关键字调用呢? 事实证明,它是通过使用我们之前学到的 instanceof 运算符来实现的。

如果使用 new 关键字调用构造函数,那么构造函数体的内部将是构造函数本身的实例。 那是很多文字才能说清楚的。 这是一些代码。

function Animal (name, energy) {
  if (this instanceof Animal === false) {
    console.warn('Forgot to call Animal with the new keyword')
  }

  this.name = name
  this.energy = energy
}

现在,如果我们使用 new 关键字重新调用函数,而不是只向函数的使用者打印警告,会发生什么呢?

function Animal (name, energy) {
  if (this instanceof Animal === false) {
    return new Animal(name, energy)
  }

  this.name = name
  this.energy = energy
}

现在无论是否使用 new 关键字调用 Animal,它都可以正常工作。

重新创建 Object.create

在这篇文章中,我们非常依赖于 Object.create 来创建委托给构造函数原型的对象。 此时,您应该知道如何在代码中使用 Object.create ,但您可能没有想到的一件事是Object.create 实际上是如何工作的。 为了让您真正了解 Object.create 的工作原理,我们将重新创建它。 首先,我们对 Object.create 的工作原理了解多少?

  1. 它接受一个对象作为参数。
  2. 它创建一个对象,该对象在查找失败时委托给参数对象。
  3. 它返回新创建的对象。

让我们从第1点开始吧。

Object.create = function (objToDelegateTo) {

}

很简单。

现在第2点 – 我们需要创建一个对象,该对象将在查找失败时委托给参数对象。 这个有点棘手。 为此,我们将使用我们对 new 关键字和原型如何在 JavaScript 中工作的知识。首先,在 Object.create 实现的主体中,我们将创建一个空函数。 然后,我们将该空函数的原型设置为参数对象。然后,为了创建一个新对象,我们将使用 new 关键字调用空函数。如果我们返回新创建的对象,也会完成第3点。

Object.create = function (objToDelegateTo) {
  function Fn(){}
  Fn.prototype = objToDelegateTo
  return new Fn()
}

有点野蛮是吧?让我们来看看吧。

当我们在上面的代码中创建一个新函数 Fn 时,它带有一个 prototype 属性。 当我们使用 new 关键字调用它时,我们知道我们将得到的是一个对象,该对象将在查找失败时委托给函数的原型。 如果我们覆盖函数的原型,那么我们可以决定在查找失败时委托给哪个对象。 所以在我们上面的例子中,我们用调用 Object.create 时传入的对象覆盖 Fn的原型,我们称之为 objToDelegateTo

请注意,我们只支持 Object.create 的单个参数。官方实现还支持第二个可选参数,该参数允许您向创建的对象添加更多属性。

箭头函数

箭头函数没有自己的 this 关键字。因此,箭头函数不能用于构造函数,如果您尝试使用 new 关键字调用箭头函数,它将抛出错误。

const Animal = () => {}

const leo = new Animal() // Error: Animal is not a constructor

另外,因为我们在上面证明了 pseudoclassical pattern(经典伪类模式) 不能与箭头函数一起使用,所以箭头函数也没有 prototype(原型) 属性。

const Animal = () => {}
console.log(Animal.prototype) // undefined

英文原文:tylermcginnis.com/beginners-g…