JavaScript Prototype 新手指南

对象基本上是 JavaScript 编程语言各个方面的基础。事实上,学习如何创建对象可能是您刚开始时学习 JavaScript 语言的第一件事。 话虽如此,为了最有效地了解 JavaScript 中的原型,我们将回到 JavaScript 初学者的角度,回顾 JavaScript 语言基础。

对象本质上是键值对。创建对象的最常见方法是使用花括号 {} 并使用点符号向对象添加属性和方法。

类似以下代码:

  1. let animal = {}
  2. animal.name = 'Leo'
  3. animal.energy = 10
  4. animal.eat = function (amount) {
  5. console.log(`${this.name} is eating.`)
  6. this.energy += amount
  7. }
  8. animal.sleep = function (length) {
  9. console.log(`${this.name} is sleeping.`)
  10. this.energy += length
  11. }
  12. animal.play = function (length) {
  13. console.log(`${this.name} is playing.`)
  14. this.energy -= length
  15. }

现在,我们需要在我们的应用程序中创建不止一种动物。那么,下一步是将该逻辑封装在一个函数中,我们可以在需要创建新动物时调用该函数。 我们将这种模式称为 函数实例化Functional Instantiation),我们将函数本身称为 构造函数constructor function),因为它负责“构造”(constructing)一个新对象。

函数实例化(Functional Instantiation)

  1. function Animal (name, energy) {
  2. let animal = {}
  3. animal.name = name
  4. animal.energy = energy
  5. animal.eat = function (amount) {
  6. console.log(`${this.name} is eating.`)
  7. this.energy += amount
  8. }
  9. animal.sleep = function (length) {
  10. console.log(`${this.name} is sleeping.`)
  11. this.energy += length
  12. }
  13. animal.play = function (length) {
  14. console.log(`${this.name} is playing.`)
  15. this.energy -= length
  16. }
  17. return animal
  18. }
  19. const leo = Animal('Leo', 7)
  20. const snoop = Animal('Snoop', 10)

现在,每当我们想要创建一个新的动物(或者说一个新的“实例”(instance)),我们所要做的就是调用我们的 Animal 函数,将动物的 nameenergy 级别作为参数传递给它。这种模式能够正常使用,也非常简单。但是,它的有什么缺点吗?

其中一个最大的缺点在于:现在我们是怎么处理实现 eatsleepplay 这些方法的?这些方法不仅是动态的,而且也是完全通用的。也就是说,每当我们创建新的动物实例时,都会重新创建这些方法。我们只是在浪费内存,使每个动物对象变得比需要的更大。

是否有别的解决办法呢?如果不是每次创建新动物实例时都重新创建这些方法,而是将它们移动到自己的对象(animalMethods)中,然后我们可以让每个动物引用该对象呢?我们可以将这种模式称为 共享方法的函数实例化Functional Instantiation with Shared Methods)。名字虽然很长,但是它准确描述了这种模式特性。

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

  1. const animalMethods = {
  2. eat(amount) {
  3. console.log(`${this.name} is eating.`)
  4. this.energy += amount
  5. },
  6. sleep(length) {
  7. console.log(`${this.name} is sleeping.`)
  8. this.energy += length
  9. },
  10. play(length) {
  11. console.log(`${this.name} is playing.`)
  12. this.energy -= length
  13. }
  14. }
  15. function Animal (name, energy) {
  16. let animal = {}
  17. animal.name = name
  18. animal.energy = energy
  19. animal.eat = animalMethods.eat
  20. animal.sleep = animalMethods.sleep
  21. animal.play = animalMethods.play
  22. return animal
  23. }
  24. const leo = Animal('Leo', 7)
  25. const snoop = Animal('Snoop', 10)

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

Object.create

现在让我们通过使用 Object.create 改进下上面的代码。简单来说,Object.create 允许您创建一个对象,该对象将在查找失败时,委托查找给另一个对象。

换句话说,Object.create 允许您创建一个对象,并且每当该对象的属性查找失败时,它可以咨询另一个对象以查看该另一个对象是否具有该属性。让我们看看下面的代码:

  1. const parent = {
  2. name: 'Stacey',
  3. age: 35,
  4. heritage: 'Irish'
  5. }
  6. const child = Object.create(parent)
  7. child.name = 'Ryan'
  8. child.age = 7
  9. console.log(child.name) // Ryan
  10. console.log(child.age) // 7
  11. console.log(child.heritage) // Irish

解释下上面的代码:因为 child 是用 Object.create(parent) 创建的,每当 child 的属性查找失败时,JavaScript 都会将查找委托给对象(parent)。这意味着即使 child 对象没有 heritage 属性,当访问 child.heritage 属性时,child.heritage 的值为 'Irish'

了解了 Object.create 的功能,如何使用它来简化之前实现的 Animal 构造函数呢?

不像之前在 Animal 中引用 animalMethods 中的方法,我们可以使用 Object.create 委托查找给 animalMethods 对象。我们给它起一个好听的名字 ,叫做: 使用 Object.create 简化后共享方法的函数实例化(Functional Instantiation with Shared Methods and Object.create)

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

  1. const animalMethods = {
  2. eat(amount) {
  3. console.log(`${this.name} is eating.`)
  4. this.energy += amount
  5. },
  6. sleep(length) {
  7. console.log(`${this.name} is sleeping.`)
  8. this.energy += length
  9. },
  10. play(length) {
  11. console.log(`${this.name} is playing.`)
  12. this.energy -= length
  13. }
  14. }
  15. function Animal (name, energy) {
  16. /* 添加 Object.create */
  17. let animal = Object.create(animalMethods)
  18. animal.name = name
  19. animal.energy = energy
  20. return animal
  21. }
  22. const leo = Animal('Leo', 7)
  23. const snoop = Animal('Snoop', 10)
  24. leo.eat(10)
  25. snoop.play(5)

现在当我们调用 leo.eat 时,JavaScript 会在 leo 对象上寻找 eat 方法。该查找将失败,然后,由于 Object.create,它将委托给 animalMethods 对象,该对象将在其中找到 eat

到现在为止还算 OK。我们可以再改进下代码。

为了跨实例共享方法,必须管理一个单独的对象(animalMethods)似乎有点 “hacky”。 我们希望这是在语言本身中就实现的常见功能。没错,这个语言本身实现的功能就是原型(prototype)。

那么 JavaScript 中的原型(prototype)到底是什么?简单地说,JavaScript 中的每个函数都有一个 prototype 属性来引用一个对象。

测试一下:

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

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

原型实例化(Prototypal Instantiation)

  1. function Animal (name, energy) {
  2. let animal = Object.create(Animal.prototype)
  3. animal.name = name
  4. animal.energy = energy
  5. return animal
  6. }
  7. Animal.prototype.eat = function (amount) {
  8. console.log(`${this.name} is eating.`)
  9. this.energy += amount
  10. }
  11. Animal.prototype.sleep = function (length) {
  12. console.log(`${this.name} is sleeping.`)
  13. this.energy += length
  14. }
  15. Animal.prototype.play = function (length) {
  16. console.log(`${this.name} is playing.`)
  17. this.energy -= length
  18. }
  19. const leo = Animal('Leo', 7)
  20. const snoop = Animal('Snoop', 10)
  21. leo.eat(10)
  22. snoop.play(5)

是否有一种恍然大悟的感觉?其实,原型(prototype)只是 JavaScript 中每个函数都拥有的一个属性,正如我们在上面看到的,它允许我们在函数的所有实例之间共享方法。我们所有的功能仍然相同,但现在不需要为所有方法管理一个单独的对象,我们可以使用另一个内置于 Animal 函数本身的对象 Animal.prototype


让我们继续往下探索

现在,我们已经知道:

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

这三个任务对于任何编程语言来说都是非常基础的。 JavaScript 真的那么糟糕,没有更简单的“内置”方式来完成同样的事情吗? 你可能猜到了,它是通过使用 new 关键字来实现的。

我们采用的缓慢、有条不紊的讲解方式的好处在于,您现在可以深入了解 JavaScript 中的 new 关键字到底在做什么。

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

  1. function Animal (name, energy) {
  2. let animal = Object.create(Animal.prototype)
  3. animal.name = name
  4. animal.energy = energy
  5. return animal
  6. }

new 关键字的一个很神奇的地方就是:当你使用 new 关键字调用一个函数时,这两行是为你隐式完成的(under the hood),同时函数实例化创建的对象(animal) 称为 this

用注释来模拟底层的隐式处理 (under the hood):使用 new 关键字调用 Animal 构造函数。代码可重写如下:

  1. function Animal (name, energy) {
  2. // const this = Object.create(Animal.prototype)
  3. this.name = name
  4. this.energy = energy
  5. // return this
  6. }
  7. const leo = new Animal('Leo', 7)
  8. const snoop = new Animal('Snoop', 10)

去掉 隐式处理under the hood) 注释的代码如下:

  1. function Animal (name, energy) {
  2. this.name = name
  3. this.energy = energy
  4. }
  5. Animal.prototype.eat = function (amount) {
  6. console.log(`${this.name} is eating.`)
  7. this.energy += amount
  8. }
  9. Animal.prototype.sleep = function (length) {
  10. console.log(`${this.name} is sleeping.`)
  11. this.energy += length
  12. }
  13. Animal.prototype.play = function (length) {
  14. console.log(`${this.name} is playing.`)
  15. this.energy -= length
  16. }
  17. const leo = new Animal('Leo', 7)
  18. const snoop = new Animal('Snoop', 10)

上述代码能够正常运行并且能够创建 this 对象的原因,是我们使用 new 关键字调用了构造函数;如果在调用函数时不使用 new,则该对象永远不会被创建,也不会被隐式返回。我们可以在下面的例子中看到这个问题:

  1. function Animal (name, energy) {
  2. this.name = name
  3. this.energy = energy
  4. }
  5. const leo = Animal('Leo', 7)
  6. console.log(leo) // undefined

这种模式的名称是 伪经典模式实例化Pseudoclassical Instantiation)。

如果 JavaScript 不是您的第一门编程语言,你可能会觉得:

“WTF 这货刚刚重新发明了一个更烂的 Class 模式” - 你

对于那些不熟悉类声明的人,Class 允许您为对象创建基础蓝本。然后,每当您创建该类的实例时,您都会获得一个具有在基础蓝本中定义好的属性和方法的对象。

听起来是不是有点熟?这基本上就是我们在上面的 Animal 构造函数中做的。 然而,我们没有使用 class 关键字,而是使用了一个常规的旧 JavaScript 函数来重新创建相同的功能。 当然,这需要一些额外的工作以及一些关于 JavaScript “幕后”发生的事情的知识,但结果是一样的。

有个好消息,JavaScript 不是一门一成不变的语言。 TC-39 委员会 不断对其进行改进和添加。这意味着即使 JavaScript 的初始版本不支持类,后续也可以添加到官方规范中。事实上,这正是 TC-39 委员会所做的。 2015 年,EcmaScript(官方 JavaScript 规范)6 发布,已经支持 Classesclass 关键字。 让我们看看上面的 Animal 构造函数在使用新的类语法时会是什么样子:

  1. class Animal {
  2. constructor(name, energy) {
  3. this.name = name
  4. this.energy = energy
  5. }
  6. eat(amount) {
  7. console.log(`${this.name} is eating.`)
  8. this.energy += amount
  9. }
  10. sleep(length) {
  11. console.log(`${this.name} is sleeping.`)
  12. this.energy += length
  13. }
  14. play(length) {
  15. console.log(`${this.name} is playing.`)
  16. this.energy -= length
  17. }
  18. }
  19. const leo = new Animal('Leo', 7)
  20. const snoop = new Animal('Snoop', 10)

非常简洁,对吧?

那么,如果这是创建类的新方法,为什么我们要花这么多时间来研究旧方法? 原因是新方法(使用 class 关键字), 实际上基于 伪经典模式pseudo-classical pattern)实现的 语法糖 。 为了充分理解 ES6 类的便捷语法,首先必须了解 伪经典模式


至此,我们已经介绍了 JavaScript 原型的基础知识。 本文的其余部分将致力于了解与原型prototype)相关的其他“须知”主题。 在另一篇文章中,我们将研究如何利用这些基础知识并使用它们来理解 继承 在 JavaScript 中的工作原理。


数组方法(Array Methods)

我们在上面深入讨论了如果你想在一个类的实例之间共享方法,你应该将这些方法附加在类(或函数)的原型上。如果我们查看 Array 类,我们可以看到同样的模式。从以往经验,你可能已经试过像这样创建了数组:

  1. const friends = []

实际上,这只是创建 Array 类的新实例的语法糖。

  1. const friendsWithSugar = []
  2. const friendsWithoutSugar = new Array()

可能你以前想过:为什么数组的每个实例能访问所有的数组内置方法(spliceslicepop 等)?

现在你可能知道了,这是因为这些方法存在于 Array.prototype 上,并且当创建 Array 的新实例时,会使用 new 关键字在查找失败时将该委托设置为 Array.prototype

尝试打印下所有 Array.prototype 上所有的方法

  1. console.log(Array.prototype)
  2. /*
  3. concat: ƒn concat()
  4. constructor: ƒn Array()
  5. copyWithin: ƒn copyWithin()
  6. entries: ƒn entries()
  7. every: ƒn every()
  8. fill: ƒn fill()
  9. filter: ƒn filter()
  10. find: ƒn find()
  11. findIndex: ƒn findIndex()
  12. forEach: ƒn forEach()
  13. includes: ƒn includes()
  14. indexOf: ƒn indexOf()
  15. join: ƒn join()
  16. keys: ƒn keys()
  17. lastIndexOf: ƒn lastIndexOf()
  18. length: 0n
  19. map: ƒn map()
  20. pop: ƒn pop()
  21. push: ƒn push()
  22. reduce: ƒn reduce()
  23. reduceRight: ƒn reduceRight()
  24. reverse: ƒn reverse()
  25. shift: ƒn shift()
  26. slice: ƒn slice()
  27. some: ƒn some()
  28. sort: ƒn sort()
  29. splice: ƒn splice()
  30. toLocaleString: ƒn toLocaleString()
  31. toString: ƒn toString()
  32. unshift: ƒn unshift()
  33. values: ƒn values()
  34. */

Object 也存在完全相同的逻辑。 所有对象都会在查找失败时委托给 Object.prototype,这就是为什么所有对象都有像 toStringhasOwnProperty 这样的方法。

静态方法(Static Methods)

到目前为止,我们已经介绍了在类的实例之间共享方法的原因和方式。但是,如果我们有一个对类很重要的方法,但不需要跨实例共享时应该怎么处理?假设我们有一个方法 nextToEat,接收一组 Animal 实例作为参数,然后根据实例的 energy 属性,返回值最小的实例 name:

  1. function nextToEat (animals) {
  2. const sortedByLeastEnergy = animals.sort((a,b) => {
  3. return a.energy - b.energy
  4. })
  5. return sortedByLeastEnergy[0].name
  6. }

nextToEat 放在 Animal.prototype 上是不合理的,因为我们不想在所有实例之间共享它。相反,我们可以将其视为辅助方法。那么,如果 nextToEat 不应该存在于 Animal.prototype 上,我们应该把它放在哪里?显而易见,我们可以将 nextToEat 放在与我们的 Animal 类相同的作用域内,然后在我们需要时引用它。

  1. class Animal {
  2. constructor(name, energy) {
  3. this.name = name
  4. this.energy = energy
  5. }
  6. eat(amount) {
  7. console.log(`${this.name} is eating.`)
  8. this.energy += amount
  9. }
  10. sleep(length) {
  11. console.log(`${this.name} is sleeping.`)
  12. this.energy += length
  13. }
  14. play(length) {
  15. console.log(`${this.name} is playing.`)
  16. this.energy -= length
  17. }
  18. }
  19. function nextToEat (animals) {
  20. const sortedByLeastEnergy = animals.sort((a,b) => {
  21. return a.energy - b.energy
  22. })
  23. return sortedByLeastEnergy[0].name
  24. }
  25. const leo = new Animal('Leo', 7)
  26. const snoop = new Animal('Snoop', 10)
  27. console.log(nextToEat([leo, snoop])) // Leo

这种写法能够正常运行,但是有一个更好的写法

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

  1. class Animal {
  2. constructor(name, energy) {
  3. this.name = name
  4. this.energy = energy
  5. }
  6. eat(amount) {
  7. console.log(`${this.name} is eating.`)
  8. this.energy += amount
  9. }
  10. sleep(length) {
  11. console.log(`${this.name} is sleeping.`)
  12. this.energy += length
  13. }
  14. play(length) {
  15. console.log(`${this.name} is playing.`)
  16. this.energy -= length
  17. }
  18. // 静态方法
  19. static nextToEat(animals) {
  20. const sortedByLeastEnergy = animals.sort((a,b) => {
  21. return a.energy - b.energy
  22. })
  23. return sortedByLeastEnergy[0].name
  24. }
  25. }

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

  1. const leo = new Animal('Leo', 7)
  2. const snoop = new Animal('Snoop', 10)
  3. console.log(Animal.nextToEat([leo, snoop])) // Leo

让我们来看看如何使用 ES5 完成同样的事情。在上面的例子中,我们看到了如何使用 static 关键字将方法直接放在类本身上。在 ES5 中,同样的实现方式就像手动将方法添加到函数对象一样简单。

  1. function Animal (name, energy) {
  2. this.name = name
  3. this.energy = energy
  4. }
  5. Animal.prototype.eat = function (amount) {
  6. console.log(`${this.name} is eating.`)
  7. this.energy += amount
  8. }
  9. Animal.prototype.sleep = function (length) {
  10. console.log(`${this.name} is sleeping.`)
  11. this.energy += length
  12. }
  13. Animal.prototype.play = function (length) {
  14. console.log(`${this.name} is playing.`)
  15. this.energy -= length
  16. }
  17. Animal.nextToEat = function (animals) {
  18. const sortedByLeastEnergy = animals.sort((a,b) => {
  19. return a.energy - b.energy
  20. })
  21. return sortedByLeastEnergy[0].name
  22. }
  23. const leo = new Animal('Leo', 7)
  24. const snoop = new Animal('Snoop', 10)
  25. console.log(Animal.nextToEat([leo, snoop])) // Leo

获取一个对象的原型(Getting the prototype of an object)

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

  1. function Animal (name, energy) {
  2. this.name = name
  3. this.energy = energy
  4. }
  5. Animal.prototype.eat = function (amount) {
  6. console.log(`${this.name} is eating.`)
  7. this.energy += amount
  8. }
  9. Animal.prototype.sleep = function (length) {
  10. console.log(`${this.name} is sleeping.`)
  11. this.energy += length
  12. }
  13. Animal.prototype.play = function (length) {
  14. console.log(`${this.name} is playing.`)
  15. this.energy -= length
  16. }
  17. const leo = new Animal('Leo', 7)
  18. const proto = Object.getPrototypeOf(leo)
  19. console.log(proto)
  20. // {constructor: ƒ, eat: ƒ, sleep: ƒ, play: ƒ}
  21. proto === Animal.prototype // true

上面的代码有两个重要的收获。

首先,我们可以观察到 proto 是一个具有 4 个方法(constructoreatsleepplay)的对象。我们使用 getPrototypeOf 方法,将 leo 实例作为参数,最后 getPrototypeOf 返回 leo 实例的原型。

除了有我们所有定义的方法( eatsleepplay),proto 对象将有一个 constructor 属性,该属性指向原始函数或创建实例的类。也就是说,因为 JavaScript 默认在原型(proto | Animal.prototype)上放置了一个 constructor 属性,任何实例都可以通过 instance.constructor 访问它们的构造函数(constructor function)。

第二个是 Object.getPrototypeOf(leo) === Animal.prototypeAnimal 构造函数有一个 prototype 属性,我们可以在其中共享所有实例的方法,getPrototypeOf 允许我们查看实例本身的原型。

  1. function Animal (name, energy) {
  2. this.name = name
  3. this.energy = energy
  4. }
  5. const leo = new Animal('Leo', 7)
  6. console.log(leo.constructor) // Logs the constructor function

结合我们之前在 Object.create 中讨论的内容,这样做的原因是因为在查找失败时,Animal 的任何实例都将委托给 Animal.prototype。因此,当您尝试访问 leo.constructor 时,leo 没有constructor 属性,因此它将该查找委托给确实具有 constructor 属性的 Animal.prototype。如果这一段没有理解,请返回并阅读上面关于 Object.create 的内容。

你可能看到过使用 __proto__ 用于获取实例的原型,那是过去的语言实现产物。 现在我们使用上面看到的 Object.getPrototypeOf(instance) 去获取实例的原型。

判断属性是否存在于原型上(Determining if a property lives on the prototype)

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

  1. function Animal (name, energy) {
  2. this.name = name
  3. this.energy = energy
  4. }
  5. Animal.prototype.eat = function (amount) {
  6. console.log(`${this.name} is eating.`)
  7. this.energy += amount
  8. }
  9. Animal.prototype.sleep = function (length) {
  10. console.log(`${this.name} is sleeping.`)
  11. this.energy += length
  12. }
  13. Animal.prototype.play = function (length) {
  14. console.log(`${this.name} is playing.`)
  15. this.energy -= length
  16. }
  17. const leo = new Animal('Leo', 7)
  18. for(let key in leo) {
  19. console.log(`Key: ${key}. Value: ${leo[key]}`)
  20. }

我们期望看到的打印结果,可能是这样的:

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

然而,实际上的打印结果:

  1. Key: name. Value: Leo
  2. Key: energy. Value: 7
  3. Key: eat. Value: function (amount) {
  4. console.log(`${this.name} is eating.`)
  5. this.energy += amount
  6. }
  7. Key: sleep. Value: function (length) {
  8. console.log(`${this.name} is sleeping.`)
  9. this.energy += length
  10. }
  11. Key: play. Value: function (length) {
  12. console.log(`${this.name} is playing.`)
  13. this.energy -= length
  14. }

这是为什么?实际上,for in 循环将遍历对象本身以及它委托给的原型的所有 可枚举属性enumerable properties)。 因为默认情况下,添加到函数原型的任何属性都是可枚举的,所以我们不仅可以看到 nameenergy,还可以看到原型上的所有方法( eatsleepplay)。

为了解决这个问题,我们要么需要指定所有原型方法都是不可枚举的,要么我们需要一个方法用于判断:属性确实位于 leo 对象本身而不是 leo 在查找失败时委托给的原型上;这个方法就是 hasOwnProperty

  1. ...
  2. const leo = new Animal('Leo', 7)
  3. for(let key in leo) {
  4. if (leo.hasOwnProperty(key)) {
  5. console.log(`Key: ${key}. Value: ${leo[key]}`)
  6. }
  7. }

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

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

如果你对 hasOwnProperty 仍然有点困惑,可以再看下这段代码:

  1. function Animal (name, energy) {
  2. this.name = name
  3. this.energy = energy
  4. }
  5. Animal.prototype.eat = function (amount) {
  6. console.log(`${this.name} is eating.`)
  7. this.energy += amount
  8. }
  9. Animal.prototype.sleep = function (length) {
  10. console.log(`${this.name} is sleeping.`)
  11. this.energy += length
  12. }
  13. Animal.prototype.play = function (length) {
  14. console.log(`${this.name} is playing.`)
  15. this.energy -= length
  16. }
  17. const leo = new Animal('Leo', 7)
  18. leo.hasOwnProperty('name') // true
  19. leo.hasOwnProperty('energy') // true
  20. leo.hasOwnProperty('eat') // false
  21. leo.hasOwnProperty('sleep') // false
  22. leo.hasOwnProperty('play') // false

检查对象是否是类的实例 (Check if an object is an instance of a Class)

有时我们想知道一个对象是否是特定类的实例,可以使用 instanceof 运算符。如果你以前从未见过它的语法,可能会觉得有点奇怪。它是这样工作的:

  1. object instanceof Class

如果 objectClass 的实例,则上面的语句将返回 true,否则将返回 false。 回到我们的 Animal 示例看下:

  1. function Animal (name, energy) {
  2. this.name = name
  3. this.energy = energy
  4. }
  5. function User () {}
  6. const leo = new Animal('Leo', 7)
  7. leo instanceof Animal // true
  8. leo instanceof User // false

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

创建能直接调用的构造函数 (Creating new agnostic constructor functions)

我们先看下这段代码有什么问题:

  1. function Animal (name, energy) {
  2. this.name = name
  3. this.energy = energy
  4. }
  5. const leo = Animal('Leo', 7)

即使是经验丰富的 JavaScript 开发人员有时也遇到上面的问题。 因为我们使用的是我们之前学到的伪经典模式pseudoclassical pattern),所以在调用 Animal 构造函数时,我们需要确保使用 new 关键字来调用它。 如果我们不这样做,那么 this 关键字将不会被创建,也不会被隐式返回。

作为复习,注释掉的行是在函数上使用 new 关键字时在底层发生的事情。

  1. function Animal (name, energy) {
  2. // const this = Object.create(Animal.prototype)
  3. this.name = name
  4. this.energy = energy
  5. // return this
  6. }

假设我们与其他开发人员在一个团队中工作,有没有办法确保我们的 Animal 构造函数始终使用 new 关键字调用?instanceof 可以帮到我们。

如果构造函数是用 new 关键字调用的,那么构造函数体内的 this 将是构造函数本身的一个实例。让我们看下下面的代码:

  1. function Animal (name, energy) {
  2. if (this instanceof Animal === false) {
  3. console.warn('Forgot to call Animal with the new keyword')
  4. }
  5. this.name = name
  6. this.energy = energy
  7. }

除了打印警告信息,我们可以使用 new 关键字重新调用函数

  1. function Animal (name, energy) {
  2. if (this instanceof Animal === false) {
  3. return new Animal(name, energy)
  4. }
  5. this.name = name
  6. this.energy = energy
  7. }

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

实现 Object.create 方法(Re-creating 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 关键字调用我们的空函数。

第三步,返回那个新创建的对象。

  1. Object.create = function (objToDelegateTo) {
  2. function Fn(){}
  3. Fn.prototype = objToDelegateTo
  4. return new Fn()
  5. }

看着有些不好理解,让我们过下这段代码的逻辑

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

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

箭头函数(Arrow Functions)

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

  1. const Animal = () => {}
  2. const leo = new Animal() // Error: Animal is not a constructor

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

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