JavaScript 原型的深入指南

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

内容简介:译者:前端小智原文:不学会怎么处理对象,你在 JavaScript 道路就就走不了多远。它们几乎是 JavaScript 编程语言每个方面的基础。事实上,学习如何创建对象可能是你刚开始学习的第一件事。

译者:前端小智

原文: tylermcginnis.com/beginners-g…

为了保证可读性,本文采用音译而非直译

不学会怎么处理对象,你在 JavaScript 道路就就走不了多远。它们几乎是 JavaScript 编程语言每个方面的基础。事实上,学习如何创建对象可能是你刚开始学习的第一件事。

想阅读更多优质文章请 猛戳GitHub博客 ,一年百来篇优质文章等着你!

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

let animal = {}
animal.name = 'Leo'
animal.energy = 10

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
}
复制代码

现在,在我们的应用程序中,我们需要创建多个 animal 。 当然,下一步是将逻辑封装,当我们需要创建新 animal 时,只需调用函数即可,我们将这种模式称为 函数的实例化(unctional 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 (或者更广泛地说,创建一个新的“实例”),我们所要做的就是调用我们的 Animal 函数,并传入参数: nameenergy 。这很有用,而且非常简单。但是,你能说这种模式的哪些缺点吗?

最大的和我们试图解决的问题与函数里面的三个方法有关 - eatsleepplay 。 这些方法中的每一种都不仅是动态的,而且它们也是完全通用的。这意味着,我们没有理由像现在一样,在创造新 animal 的时候重新创建这些方法。我们只是在浪费内存,让每一个新建的对象都比实际需要的还大。

你能想到一个解决方案吗? 如果不是在每次创建新动物时重新创建这些方法,我们将它们移动到自己的对象然后我们可以让每个动物引用该对象,该怎么办? 我们可以将此模式称为 函数实例化与共享方法

函数实例化与共享方法

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 时,它会从 parent 对象中找到对应 heritage 并打印出来。

现在如何使用 Object.create 来简化之前的 Animal 代码? 好吧,我们可以使用 Object.create 来委托给 animalMethods 对象,而不是像我们现在一样逐一向 animal 添加所有共享方法。 为了B 格一点,就叫做 使用共享方法 和 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 方法,因为 leo 中没有 eat 方法,所以查找将失败,由于 Object.create ,它将委托给 animalMethods 对象,所以会从 animalMethods 对象上找到 eat 方法。

到现在为止还挺好。尽管如此,我们仍然可以做出一些改进。为了跨实例共享方法,必须管理一个单独的对象( animalMethods )似乎有点“傻哈”。我们希望这在语言本身中实现的一个常见特,所以就需要引出下一个属性 - prototype

那么究竟 JavaScript 中的 prototype 是什么? 好吧,简单地说,JavaScript 中的每个函数都有一个引用对象的 prototype 属性。

function doThing () {}
console.log(doThing.prototype) // {}
复制代码

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

原型(prototype)实例化

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)
复制代码

同样, prototype 只是 JavaScript 中的每个函数都具有的一个属性,正如我们前面看到的,它允许我们跨函数的所有实例共享方法。我们所有的功能仍然是相同的,但是现在我们不必为所有的方法管理一个单独的对象,我们只需要使用 Animal 函数本身内置的另一个对象 Animal.prototype

更进一步

现在我们知道三个点:

  1. 如何创建构造函数。

  2. 如何向构造函数的原型添加方法。

  3. 如何使用 Object.create 将失败的查找委托给函数的原型。

这三个点对于任何编程语言来说都是非常基础的。JavaScript 真的有那么糟糕,以至于没有更简单的方法来完成同样的事情吗?正如你可能已经猜到的那样,现在已经有了,它是通过使用 new 关键字来实现的。

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

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

  return animal   // 2
}
复制代码

关于 new ,有一件很酷的事情——当你使用 new 关键字调用一个函数时,以下编号为 12 两行代码将隐式地(在底层)为你完成,所创建的对象被称为 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)
复制代码

再次说明,之所以这样做,并且这个对象是为我们创建的,是因为我们用 new 关键字调用了构造函数。如果在调用函数时省略 new ,则永远不会创建该对象,也不会隐式地返回该对象。我们可以在下面的例子中看到这个问题。

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

const leo = Animal('Leo', 7)
console.log(leo) // undefined
复制代码

这种模式称为 伪类实例化

对于那些不熟悉的人, 允许你为对象创建蓝图。 然后,每当你创建该类的实例时,你可以访问这个对象中定义的属性和方法。

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

这是个好消息。 JavaScript 不是一种死语言。 TC-39委员会不断改进和补充。 这意味着即使JavaScript的初始版本不支持类,也没有理由将它们添加到官方规范中。 事实上,这正是TC-39委员会所做的。 2015 年,发布了EcmaScript(官方JavaScript规范)6,支持类和 class 关键字。 让我们看看上面的 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
  }
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)
复制代码

这个相对前面的例子,是相对简单明了的。

因此,如果这是创建类的新方法,为什么我们花了这么多时间来复习旧的方式呢? 原因是因为新方法(使用 class 关键字)主要只是我们称之为 伪类实例化模式 现有方式的“语法糖”。 为了完全理解 ES6 类的便捷语法,首先必须理解 伪类实例化模式

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

数组方法

我们在上面深入讨论了如何在一个类的实例之间共享方法,你应该将这些方法放在类(或函数)原型上。 如果我们查看 Array 类,我们可以看到相同的模式。

onst friends = []
复制代码

以为是代替使用 new Array() 的一个语法糖。

const friendsWithSugar = []

const friendsWithoutSugar = new Array()
复制代码

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

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

我们可以打印 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.prototype ,这就是所有对象都有 toStringhasOwnProperty 等方法的原因

静态方法

到目前为止,我们已经讨论了为什么以及如何在类的实例之间共享方法。但是,如果我们有一个对类很重要的方法,但是不需要在实例之间共享该方法怎么办?例如,如果我们有一个函数,它接收一系列 Animal 实例,并确定下一步需要喂养哪一个呢?我们这个方法叫做 nextToEat

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

  return sortedByLeastEnergy[0].name
}
复制代码

因为我们不希望在所有实例之间共享 nextToEat ,所以在 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
复制代码

这是可行的,但是还有一个更好的方法。

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

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 作为静态属性,所以它存在于 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 proto  = Object.getPrototypeOf(leo)

console.log(proto )
// {constructor: ƒ, eat: ƒ, sleep: ƒ, play: ƒ}

proto === Animal.prototype // true
复制代码

上面的代码有两个重要的要点。

首先,你将注意到 proto 是一个具有 4 个方法的对象, constructoreatsleepplay 。这是有意义的。我们使用 getPrototypeOf 传递实例, leo 取回实例原型,这是我们所有方法的所在。

这也告诉了我们关于 prototype 的另一件事,我们还没有讨论过。默认情况下, prototype 对象将具有一个 constructor 属性,该属性指向初始函数或创建实例的类。这也意味着因为 JavaScript 默认在原型上放置构造函数属性,所以任何实例都可以通过。

第二个重要的点是: 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.constructor 时, leo 没有 constructor 属性,因此它会将该查找委托给 Animal.prototype ,而 Animal.prototype 确实具有构造函数属性。

你之前可能看过使用 __proto__ 用于获取实例的原型,这是过去的遗物。 相反,如上所述使用 Object.getPrototypeOf(instance)

判断原型上是否包含某个属性

在某些情况下,你需要知道属性是否存在于实例本身上,还是存在于对象委托的原型上。 我们可以通过循环打印我们创建的 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 可以帮助我们的地方。

...

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 Animal 为 true,因为 Object.getPrototypeOf(leo) === Animal.prototype 。 另外, leo instanceof User 为 false,因为 Object.getPrototypeOf(leo) !== User.prototype

创建新的不可知的构造函数

你能找出下面代码中的错误吗

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

const leo = Animal('Leo', 7)
复制代码

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

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

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

  this.name = name
  this.energy = energy

  // return this
}
复制代码

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

如果使用 new 关键字调用构造函数,那么构造函数体的内部 this 将是构造函数本身的实例。

function Aniam (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. 它返回新创建的对象。

    Object.create = function (objToDelegateTo) {

    }

现在,我们需要创建一个对象,该对象将在失败的查找中委托给参数对象。 这个有点棘手。 为此,我们将使用 new 关键字相关的知识。

首先,在 Object.create 主体内部创建一个空函数。 然后,将空函数的 prototype 设置为等于传入参数对象。 然后,返回使用 new 关键字调用我们的空函数。

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
复制代码

另外,因为我们在上面说明了 伪类实例模 式不能与箭头函数一起使用,所以箭头函数也没有原型属性。

const Animal = () => {}
console.log(Animal.prototype) // undefined
复制代码

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具Fundebug。

你的点赞是我持续分享好东西的动力,欢迎点赞!

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

github.com/qq449245884…

我是小智,公众号「大迁世界」作者, 对前端技术保持学习爱好者。我会经常分享自己所学所看的干货 ,在进阶的路上,共勉!

关注公众号,后台回复 福利 ,即可看到福利,你懂的。

JavaScript 原型的深入指南

以上所述就是小编给大家介绍的《JavaScript 原型的深入指南》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

企业应用架构模式

企业应用架构模式

Martin Fowler、王怀民、周斌 / 王怀民、周斌 / 机械工业出版社 / 2004-7 / 49.00元

本书作者是当今面向对象软件开发的权威,他在一组专家级合作者的帮助下,将40多种经常出现的解决方案转化成模式,最终写成这本能够应用于任何一种企业应用平台的、关于解决方案的、不可或缺的手册。本书获得了2003年度美国软件开发杂志图书类的生产效率奖和读者选择奖。本书分为两大部分。第一部分是关于如何开发企业应用的简单介绍。第二部分是本书的主体,是关于模式的详细参考手册,每个模式都给出使用方法和实现信息,并一起来看看 《企业应用架构模式》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具