JavaScript 原型初学者指南让我们更深入地了解。

2025-05-28

JavaScript 原型初学者指南

让我们。走。更深。

如果不处理对象,JavaScript 就无法走得很远。对象几乎是 JavaScript 编程语言各个方面的基础。在本文中,你将学习各种实例化新对象的模式,并逐步深入理解 JavaScript 的原型。

这是我们高级 JavaScript课程的一部分。如果你喜欢这篇文章,欢迎查看。

视频

邮政

如果不处理对象,JavaScript 就无法走得很远。对象几乎是 JavaScript 编程语言各个方面的基础。事实上,学习如何创建对象可能是你刚开始学习 JavaScript 时最先学习的内容之一。话虽如此,为了最有效地学习 JavaScript 中的原型,我们将引导我们内心的初级开发者,回归基础。

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

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
}
Enter fullscreen mode Exit fullscreen mode

很简单。现在我们的应用中很可能需要创建多个动物。下一步自然是将逻辑封装到一个函数中,每当我们需要创建新动物时,就可以调用这个函数。我们将这种模式称为“构造函数” 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)
Enter fullscreen mode Exit fullscreen mode

"I thought this was an Advanced JavaScript course...?" - Your brain 是的,我们会到达那里。

现在,每当我们想要创建一个新动物(或者更广泛地说是一个新的“实例”)时,我们所要做的就是调用我们的Animal函数,并将动物的nameenergy级别传递给它。这很有效,而且非常简单。但是,你能发现这个模式有什么缺点吗?最大的缺点,也是我们将要尝试解决的缺点,与这三个方法有关—— eatsleepplay。这每一个方法不仅是动态的,而且是完全通用的。这意味着,我们没有必要像现在这样,在创建新动物时重新创建这些方法。我们只是在浪费内存,并使每个动物对象都变得比实际需要的更大。你能想到一个解决方案吗?如果我们不是在每次创建新动物时都重新创建这些方法,而是将它们移动到它们自己的对象中,那么我们就可以让每个动物都引用该对象,怎么样?我们可以将这种模式称为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)
Enter fullscreen mode Exit fullscreen mode

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

对象.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
Enter fullscreen mode Exit fullscreen mode

因此,在上面的例子中,由于child是使用 创建的Object.create(parent),因此每当 的属性查找失败时child,JavaScript 都会将查找委托给该parent对象。这意味着,即使child本身没有heritage属性,parent但当你登录时,child.heritage你也会获得parent的继承,也就是Irish

现在有了Object.create工具棚,我们该如何使用它来简化Animal之前的代码呢?好吧,与其像现在这样一个接一个地给动物添加所有共享方法,animalMethods不如使用 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)
Enter fullscreen mode Exit fullscreen mode

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

到目前为止,一切都很好。不过,我们仍然可以做一些改进。animalMethods为了在实例之间共享方法,必须管理一个单独的对象 ( ),这似乎有点“hacky”。​​这似乎是一个你希望在语言本身中实现的常见功能。事实证明确实如此,而且这也是你来这里的原因—— prototype

那么prototypeJavaScript 中到底是什么呢?简单来说,JavaScript 中的每个函数都有一个prototype引用对象的属性。有点儿虎头蛇尾,对吧?自己测试一下吧。

function doThing () {}
console.log(doThing.prototype) // {}
Enter fullscreen mode Exit fullscreen mode

如果我们不创建一个单独的对象来管理我们的方法(就像我们在 中所做的那样animalMethods),而是将每个方法放在Animal函数的原型上,会怎么样?那么我们要做的就是,不再使用 Object.create 来委托animalMethods,而是使用它来委托Animal.prototype。我们将这种模式称为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)
Enter fullscreen mode Exit fullscreen mode

👏👏👏 希望你刚才恍然大悟。再次强调,prototype只是 JavaScript 中每个函数都具有的一个属性,正如我们上面所见,它允许我们在函数的所有实例之间共享方法。所有功能仍然相同,但现在我们不必为所有方法管理一个单独的对象,而是可以使用函数Animal本身内置的另一个对象Animal.prototype


让我们。走。更深。

至此,我们知道了三件事:

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

这三个任务对于任何编程语言来说似乎都是相当基础的。JavaScript 真的那么糟糕,以至于没有更简单的“内置”方法来完成同样的事情吗?你现在可能已经猜到了,确实有,那就是使用new关键字。

我们采取的缓慢而有条不紊的方法的好处是,您现在可以深入了解newJavaScript 中的关键字在底层的作用。

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

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

  return animal
}
Enter fullscreen mode Exit fullscreen mode

很酷的是new- 当您使用关键字调用函数时new,这两行代码会隐式地(“在后台”)为您完成,并且创建的对象被称为this

使用注释来展示幕后发生的事情,并假设使用关键字Animal调用构造函数new,它可以被重写为这样。

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)
Enter fullscreen mode Exit fullscreen mode

并且没有“幕后”评论

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)
Enter fullscreen mode Exit fullscreen mode

再次强调,之所以能成功并this创建对象,是因为我们用关键字 调用了构造函数。如果在调用函数时new中断,该对象就永远不会被创建,也不会被隐式返回。我们可以在下面的例子中看到这个问题。newthis

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

const leo = Animal('Leo', 7)
console.log(leo) // undefined
Enter fullscreen mode Exit fullscreen mode

此模式的名称是Pseudoclassical Instantiation

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

“这家伙刚刚重新创建了一个更糟糕的 Class 版本” - 你

对于不熟悉的人来说,类可以让你为对象创建蓝图。然后,每当你创建该类的实例时,你都会得到一个具有蓝图中定义的属性和方法的对象。

听起来很熟悉?这基本上就是我们上面构造函数的操作Animal。只不过,class我们没有使用关键字,而是用一个普通的 JavaScript 函数来重现同样的功能。当然,这需要一些额外的工作,也需要一些 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)
Enter fullscreen mode Exit fullscreen mode

很干净吧?

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


至此,我们已经介绍了 JavaScript 原型的基础知识。本文的剩余部分将致力于帮助您理解与之相关的其他“值得了解”的主题。在另一篇文章中,我们将探讨如何利用这些基础知识来理解 JavaScript 中的继承机制。


数组方法

我们上面深入讨论了如何在类的实例之间共享方法,应该将这些方法放在类(或函数)的原型上。如果我们观察一下这个类,就能看到同样的模式Array。以前你可能像这样创建数组

const friends = []
Enter fullscreen mode Exit fullscreen mode

事实证明,这只是创建类new实例的糖衣而已Array

const friendsWithSugar = []

const friendsWithoutSugar = new Array()
Enter fullscreen mode Exit fullscreen mode

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

嗯,正如您现在所知道的,这是因为这些方法仍然存在,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()
*/

Enter fullscreen mode Exit fullscreen mode

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

静态方法

到目前为止,我们已经介绍了在类的实例之间共享方法的原因和方法。但是,如果我们有一个对类很重要的方法,但不需要跨实例共享,该怎么办?例如,如果我们有一个函数,它接受一个Animal实例数组作为参数,并确定下一个需要传入哪个实例,该怎么办?我们将其命名为nextToEat

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

  return sortedByLeastEnergy[0].name
}
Enter fullscreen mode Exit fullscreen mode

由于我们不想在所有实例之间共享它,所以让它nextToEat继续存在是没有意义的。相反,我们可以把它看作是一个辅助方法。那么,如果不应该让它继续存在,我们应该把它放在哪里呢?答案显而易见:我们可以把它放在与类相同的作用域内,然后在需要的时候像平常一样引用它。Animal.prototypenextToEatAnimal.prototypenextToEatAnimal

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
Enter fullscreen mode Exit fullscreen mode

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

当您拥有一个特定于类本身的方法,但不需要在该类的实例之间共享时,您可以将其添加为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
  }
}
Enter fullscreen mode Exit fullscreen mode

现在,因为我们在类中添加了nextToEat一个属性,所以它存在于类本身(而不是其原型)中,并且可以使用进行访问staticAnimalAnimal.nextToEat

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

console.log(Animal.nextToEat([leo, snoop])) // Leo
Enter fullscreen mode Exit fullscreen mode

因为我们在整篇文章中都遵循了类似的模式,所以让我们看看如何使用 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
Enter fullscreen mode Exit fullscreen mode

获取对象的原型

无论使用哪种模式创建对象,都可以使用该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
Enter fullscreen mode Exit fullscreen mode

上面的代码有两个重要的启示。

首先,你会注意到它proto是一个包含 4 个方法的对象,constructor分别是eatsleep、 和play。这很合理。我们getPrototypeOf传入了实例,leo并返回了实例的原型,也就是所有方法的存放位置。这还告诉我们一件prototype我们之前没有提到的事情。默认情况下,该prototype对象会有一个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
Enter fullscreen mode Exit fullscreen mode

为了与我们之前讨论的 相呼应Object.create,它之所以有效,是因为 的任何实例在查找失败时Animal都会委托给Animal.prototype。因此,当您尝试访问 时leo.constructorleo由于 没有constructor属性,所以它会将查找委托给 ,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]}`)
}
Enter fullscreen mode Exit fullscreen mode

你期望看到什么?很可能是这样的——

Key: name. Value: Leo
Key: energy. Value: 7
Enter fullscreen mode Exit fullscreen mode

然而,如果你运行代码,你会看到这样的情况 -

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
}
Enter fullscreen mode Exit fullscreen mode

这是为什么呢?因为for in循环会遍历对象本身以及它委托给的原型上的所有可枚举属性name。因为默认情况下,添加到函数原型的任何属性都是可枚举的,所以我们不仅可以看到和energy,还可以看到原型上的所有方法 - eatsleepplay。为了解决这个问题,我们要么需要指定所有原型方法都是不可枚举的,要么需要一种方法,仅在属性位于对象leo本身而不是委托给的原型上时,leo在查找失败时才 console.log 。这就是hasOwnProperty可以帮助我们的地方。

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

...

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

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

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

Key: name. Value: Leo
Key: energy. Value: 7
Enter fullscreen mode Exit fullscreen mode

如果您仍然有点困惑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
Enter fullscreen mode Exit fullscreen mode

检查对象是否是类的实例

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

object instanceof Class
Enter fullscreen mode Exit fullscreen mode

object如果是 的实例,则上述语句将返回 true;Class如果不是,则返回 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
Enter fullscreen mode Exit fullscreen mode

它的工作原理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)
Enter fullscreen mode Exit fullscreen mode

即使是经验丰富的 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
}
Enter fullscreen mode Exit fullscreen mode

这似乎是一个非常重要的细节,不能留给其他开发人员去记住。假设我们和其他开发人员在一个团队里工作,有没有办法确保我们的Animal构造函数总是通过关键字调用new?事实证明是有的,那就是使用instanceof我们之前学到的运算符。

如果构造函数是用new关键字 调用的,那么this构造函数体内部就是instanceof构造函数本身。这可真是个大问题。以下是一些代码。

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
}
Enter fullscreen mode Exit fullscreen mode

new现在,如果我们重新调用该函数,但这次使用关键字,那么不仅仅是向函数的使用者记录警告,会怎么样?

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

  this.name = name
  this.energy = energy
}
Enter fullscreen mode Exit fullscreen mode

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

重新创建 Object.create

在本文中,我们大量依赖于Object.create委托构造函数原型来创建对象。现在,你应该知道如何Object.create在代码中使用它,但你可能还没想过Object.create它背后的工作原理。为了让你真正理解它的工作原理Object.create,我们将自己重新创建一个。首先,我们对它的Object.create工作原理了解多少?

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

让我们从#1开始。

Object.create = function (objToDelegateTo) {

}
Enter fullscreen mode Exit fullscreen mode

够简单了。

现在进行 #2 操作——我们需要创建一个对象,当查找失败时,该对象会委托给参数对象。这个操作稍微有点棘手。为此,我们需要运用newJavaScript 中关键字和原型的工作原理。首先,在实现体中Object.create,我们将创建一个空函数。然后,我们将该空函数的原型设置为参数对象。接下来,为了创建一个新对象,我们将使用new关键字调用该空函数。如果我们返回这个新创建的对象,那么 #3 操作也就算完成了。

Object.create = function (objToDelegateTo) {
  function Fn(){}
  Fn.prototype = objToDelegateTo
  return new Fn()
}
Enter fullscreen mode Exit fullscreen mode

太疯狂了。我们一起去看看吧。

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

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

箭头函数

箭头函数没有自己的this关键字。因此,箭头函数不能作为构造函数,并且如果尝试使用new关键字调用箭头函数,则会引发错误。

const Animal = () => {}

const leo = new Animal() // Error: Animal is not a constructor
Enter fullscreen mode Exit fullscreen mode

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

const Animal = () => {}
console.log(Animal.prototype) // undefined
Enter fullscreen mode Exit fullscreen mode

这最初发表在TylerMcGinnis.com上,是其高级 JavaScript课程的一部分

文章来源:https://dev.to/tylermcginnis/a-beginners-guide-to-javascripts-prototype-5kk
PREV
2018 年 React.js 综合指南
NEXT
使用 OOP 简化 WordPress 的 functions.php