JavaScript Prototype(原型) 新手指南

2018-11-1309:15:33WEB前端开发Comments2,433 views字数 16802阅读模式

如果不处理对象,您就无法在 JavaScript 方面取得很大进展。它们几乎是 JavaScript 编程语言的所有方面的基础。在这篇文章中,您将了解用于实例化新对象的各种模式,在此过程中,您将逐步深入了解 JavaScript 的原型。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

基础入门

如果不处理对象,您就无法在 JavaScript 方面取得很大进展。它们几乎是 JavaScript 编程语言的所有方面的基础。事实上,学习如何创建对象可能是你刚开始学习的第一件事。话虽如此,为了最有效地学习 JavaScript 中的原型,我们将从基础开始。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

首先,对象是键/值对。创建对象的最常用方法是使用花括号{},并使用点表示法向对象添加属性和方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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
}

这个很简单。现在,我们在应用程序中我们需要创建多个动物。很自然地,下一步就是将逻辑封装到一个函数中,以便我们在需要创建新动物时调用这个函数。我们将调用这个模式 Functional Instantiation(函数实例化),并将函数本身称为 constructor function(构造函数) ,因为它负责“构造”一个新对象。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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 函数,将动物的 name 和energy 传递给这个函数。这非常有效,而且非常简单。但是,你有发现这种模式的不足之处吗?我们要尝试解决的最大的问题与三种方法有关 – eatsleep 和 play。这些方法中的每一种都不仅是动态的,而且它们也是完全通用的。这意味着没有理由重新创建这些方法,正如我们在创建新动物时所做的那样。我们只是在浪费内存,让每个动物物体都比它需要的更大。你能想到一个解决方案吗?
如果我们每次创建一个新动物时不需要重新创建这些方法,而是将它们移动到它们自己的对象上,那么我们就可以让每个动物引用那个对象了?我们可以把这种模式称为 Functional Instantiation with Shared Methods(共享方法的函数实例化) 。描述起来有点啰嗦 ?‍ 。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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 函数中引用该对象,我们现在已经解决了内存浪费和动物对象过大的问题。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

Object.create

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

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 属性,即 Irish文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

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 方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

那么究竟什么是 JavaScript 的 prototype(原型)呢? 简单地说,JavaScript 中的每个函数都有一个引用对象的 prototype 属性。 我们来亲自测试一下。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

如果不是创建一个单独的对象(比如我们正在使用的 animalMethods )来管理我们的方法,也就是我们只是将每个方法放在 Animal 函数的 prototype(原型) 对象上,该怎么办呢?我们所要做的就是不使用 Object.create 委托给 animalMethods,我们可以用使用来委托Animal.prototype。 我们将这种模式称为 Prototypal Instantiation(原型实例化)。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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 。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html


更深的,走起!

首先,我们需要知道三件事:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

这三个任务似乎是任何编程语言的基础。 JavaScript 是否真的那么糟糕,没有更简单,“内置”的方式来完成同样的事情? 正如你可能已经猜测的那样,它是通过使用 new 关键字。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

我们采用的这种缓慢而有条理的方法的好处是,您现在可以深入了解 JavaScript 中的 new 关键字在幕后的作用。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

回顾一下我们的 Animal 构造函数,最重要的两个部分是创建对象并返回它。 如果不使用 Object.create 创建对象,我们将无法在查找失败时委托给函数的原型。 如果没有 return 语句,我们将永远不会返回创建的对象。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

new 有一个很酷的地方——当您使用 new 关键字调用函数时,注释掉的这两行代码是隐式(引擎)完成的,创建的对象称为 this文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

使用注释来显示在幕后发生的事情并假设使用 new 关键字调用 Animal 构造函数,可以将其重写为这样:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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)

去掉注释后:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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 ,则该对象永远不会创建,也不会隐式返回。我们可以在下面的例子中看到这个问题。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

function Animal (name, energy) {
this.name = name
this.energy = energy
}
const leo = Animal('Leo', 7)
console.log(leo) // undefined

此模式的名称是 Pseudoclassical Instantiation(伪类实例化) 。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

如果 JavaScript 不是您的第一种编程语言,您可能会有点不安。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

“WTF这个家伙只是重新创造了一个更糟糕的版本” – 你文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

对于那些不熟悉的人,Class(类) 允许您为对象创建模板。然后,无论何时创建该类的实例,都会获得一个具有模板中定义的属性和方法的对象。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

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

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)

很干净是吧?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

因此,如果这是创建类的新方法,为什么我们前面花了这么多时间来讨论旧的方式呢? 原因是新的方式(使用 class 关键字)只是经典伪类模式的 “语法糖”。 为了完全理解 ES6 类的便捷语法,首先必须理解经典的伪类模式。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

关于 ES6 Classes(类) 更多相关信息可以查看 面向对象的 JavaScript – 深入了解 ES6 类文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html


我们已经介绍了 JavaScript 原型的基础知识。 本文的其余部分将致力于加深理解相关知识的主题。 在另一篇文章中,我们将看看如何利用这些基础知识并使用它们来理解 JavaScript 中继承的工作原理。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html


数组方法

我们在上面深入讨论了如何在类的实例之间共享方法,您应该将这些方法放在类(或函数)原型上。 如果我们查看 Array 类,我们可以看到相同的模式。 从历史上看,您可能已经创建了这样的数组:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

const friends = []

事实证明,这只是创建一个新的 Array 类实例的语法糖。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

const friendsWithSugar = []
const friendsWithoutSugar = new Array()

您可能从未想过:数组的每个实例是如何具有所有内置方法的(splice , slicepop 等)?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

正如您现在所知,这是因为这些方法存在于 Array.prototype 上,当您创建一个新的 Array 实例时,您使用 new 关键字在查找失败时将该委托设置为 Array.prototype 。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

我们可以通过简单地 console.log(Array.prototype) 来查看所有数组的方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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 ,这就是所有对象都有 toString 和hasOwnProperty 等方法的原因。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

静态方法

到目前为止,我们已经介绍了为什么,以及如何在类的实例之间共享方法。 但是,如果我们有一个对 Class 很重要但不需要又跨实例共享的方法,该怎么办呢? 例如,如果我们有一个函数,它接收一系列 Animal 实例并决定下一个需要喂食的对象,会怎样? 我们将其称为 nextToEat文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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 类相同的作用域中,然后像我们平常那样,在需要时引用它。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

现在这可行,但有更好的方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

只要有一个特定于类本身的方法,但不需要在该类的实例之间共享,就可以将其添加为类的 static(静态) 属性。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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 进行访问。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

因为我们在这篇文章中都遵循了类似的模式,让我们来看看如何使用 ES5 完成同样的事情。 在上面的例子中,我们看到了如何使用 static 关键字将方法直接放在类本身上。 使用ES5,同样的模式就像手动将方法添加到函数对象一样简单。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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 方法完成获取该对象的原型。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

上面的代码有两个要点。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

上面的第二个要点是 Object.getPrototypeOf(leo) === Animal.prototype 。这也是有道理的。 Animal 构造函数有一个 prototype(原型) 属性,我们可以在所有实例之间共享方法,getPrototypeOf 允许我们查看实例本身的原型。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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 。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

您可能以前看到过使用 __proto__ 获取实例的原型。 这是过去的遗物。 现在,如上所述使用 Object.getPrototypeOf(instance) 获取实例的原型。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

在某些情况下,您需要知道属性是否存在于实例本身上,还是存在于对象委托的原型上。 我们可以通过循环我们创建的 leo 对象来知道这一点。假设目标是循环 leo 并记录其所有键和值。使用 for in 循环,可能看起来像这样。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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]}`)
}

你期望看到什么?最有可能的是,它是这样的 –文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

但是,如果你运行代码,你看到的是这样的 –文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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 循环将循环遍历对象本身以及它所委托的原型的所有 可枚举属性 。 因为默认情况下,您添加到函数原型的任何属性都是可枚举的,我们不仅会看到name 和 energy ,还会看到原型上的所有方法 – eatsleep 和 play 。 要解决这个问题,我们需要指定所有原型方法都是不可枚举的,或者如果属性在 leo 对象本身上而不是 leo 查找失败时委托给的原型上。 hasOwnProperty 可以帮助我们实现这个需求。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

hasOwnProperty 是每个对象上的一个属性,它返回一个布尔值,指示对象是否具有指定的属性作为其自身的属性,而不是对象委托给的原型。 这正是我们所需要的。 现在有了这些新知识,我们可以修改我们的代码,以便利用 for in 循环中的 hasOwnProperty 。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

而现在我们看到的只是 leo 对象本身的属性,而不是 leo 原型中的方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

如果你仍然对 hasOwnProperty 感到困惑,这里有一些代码可以帮你消除困惑。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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 运算符。 用例非常简单,但如果您以前从未见过它,实际的语法有点奇怪。 它的工作原理如下文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

object instanceof Class

如果 object 是 Class 的实例,则上面的语句将返回 true ,否则返回 false 。回到我们的 Animal 示例,我们会有类似的东西。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

创建新的不可知构造函数

你能发现下面代码中的错误吗?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

function Animal (name, energy) {
this.name = name
this.energy = energy
}
const leo = Animal('Leo', 7)

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

作为复习,注释掉的行是在函数上使用 new 关键字时幕后所做的事情。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

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

如果使用 new 关键字调用构造函数,那么构造函数体的内部将是构造函数本身的实例。 那是很多文字才能说清楚的。 这是一些代码。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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 关键字重新调用函数,而不是只向函数的使用者打印警告,会发生什么呢?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

现在无论是否使用 new 关键字调用 Animal,它都可以正常工作。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

重新创建 Object.create

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

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

让我们从第1点开始吧。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

Object.create = function (objToDelegateTo) {
}

很简单。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

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

有点野蛮是吧?让我们来看看吧。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

请注意,我们只支持 Object.create 的单个参数。官方实现还支持第二个可选参数,该参数允许您向创建的对象添加更多属性。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

箭头函数

箭头函数没有自己的 this 关键字。因此,箭头函数不能用于构造函数,如果您尝试使用 new 关键字调用箭头函数,它将抛出错误。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

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

另外,因为我们在上面证明了 pseudoclassical pattern(经典伪类模式) 不能与箭头函数一起使用,所以箭头函数也没有 prototype(原型) 属性。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html

const Animal = () => {}
console.log(Animal.prototype) // undefined
文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/7854.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/gcs/7854.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定