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
}
很简单。现在我们的应用中很可能需要创建多个动物。下一步自然是将逻辑封装到一个函数中,每当我们需要创建新动物时,就可以调用这个函数。我们将这种模式称为“构造函数” 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)
"I thought this was an Advanced JavaScript course...?" - Your brain
是的,我们会到达那里。
现在,每当我们想要创建一个新动物(或者更广泛地说是一个新的“实例”)时,我们所要做的就是调用我们的Animal
函数,并将动物的name
和energy
级别传递给它。这很有效,而且非常简单。但是,你能发现这个模式有什么缺点吗?最大的缺点,也是我们将要尝试解决的缺点,与这三个方法有关—— eat
、sleep
和play
。这每一个方法不仅是动态的,而且是完全通用的。这意味着,我们没有必要像现在这样,在创建新动物时重新创建这些方法。我们只是在浪费内存,并使每个动物对象都变得比实际需要的更大。你能想到一个解决方案吗?如果我们不是在每次创建新动物时都重新创建这些方法,而是将它们移动到它们自己的对象中,那么我们就可以让每个动物都引用该对象,怎么样?我们可以将这种模式称为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
函数内部引用该对象,我们现在解决了内存浪费和动物对象过大的问题。
对象.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
属性,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)
📈 现在,当我们调用 时leo.eat
,JavaScript 会在对象上查找该eat
方法leo
。该查找会失败,然后,由于 Object.create 的作用,它会委托给animalMethods
对象,并在该对象中找到eat
。
到目前为止,一切都很好。不过,我们仍然可以做一些改进。animalMethods
为了在实例之间共享方法,必须管理一个单独的对象 ( ),这似乎有点“hacky”。这似乎是一个你希望在语言本身中实现的常见功能。事实证明确实如此,而且这也是你来这里的原因—— prototype
。
那么prototype
JavaScript 中到底是什么呢?简单来说,JavaScript 中的每个函数都有一个prototype
引用对象的属性。有点儿虎头蛇尾,对吧?自己测试一下吧。
function doThing () {}
console.log(doThing.prototype) // {}
如果我们不创建一个单独的对象来管理我们的方法(就像我们在 中所做的那样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)
👏👏👏 希望你刚才恍然大悟。再次强调,prototype
只是 JavaScript 中每个函数都具有的一个属性,正如我们上面所见,它允许我们在函数的所有实例之间共享方法。所有功能仍然相同,但现在我们不必为所有方法管理一个单独的对象,而是可以使用函数Animal
本身内置的另一个对象Animal.prototype
。
让我们。走。更深。
至此,我们知道了三件事:
1) 如何创建构造函数。2
) 如何向构造函数的原型添加方法。3
) 如何使用 Object.create 将失败的查找委托给函数的原型。
这三个任务对于任何编程语言来说似乎都是相当基础的。JavaScript 真的那么糟糕,以至于没有更简单的“内置”方法来完成同样的事情吗?你现在可能已经猜到了,确实有,那就是使用new
关键字。
我们采取的缓慢而有条不紊的方法的好处是,您现在可以深入了解new
JavaScript 中的关键字在底层的作用。
回顾我们的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
。
使用注释来展示幕后发生的事情,并假设使用关键字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)
并且没有“幕后”评论
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
this
function Animal (name, energy) {
this.name = name
this.energy = energy
}
const leo = Animal('Leo', 7)
console.log(leo) // undefined
此模式的名称是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)
很干净吧?
那么,如果这是创建类的新方法,我们为什么要花这么多时间去复习旧方法呢?原因在于,新方法(使用class
关键字)本质上只是我们称之为伪经典模式的现有方法的“语法糖”。为了完全理解 ES6 类的便捷语法,首先必须理解伪经典模式。
至此,我们已经介绍了 JavaScript 原型的基础知识。本文的剩余部分将致力于帮助您理解与之相关的其他“值得了解”的主题。在另一篇文章中,我们将探讨如何利用这些基础知识来理解 JavaScript 中的继承机制。
数组方法
我们上面深入讨论了如何在类的实例之间共享方法,应该将这些方法放在类(或函数)的原型上。如果我们观察一下这个类,就能看到同样的模式Array
。以前你可能像这样创建数组
const 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
在查找失败时都会委托给 ,这就是为什么所有对象都有像toString
和 这样的方法hasOwnProperty
。
静态方法
到目前为止,我们已经介绍了在类的实例之间共享方法的原因和方法。但是,如果我们有一个对类很重要的方法,但不需要跨实例共享,该怎么办?例如,如果我们有一个函数,它接受一个Animal
实例数组作为参数,并确定下一个需要传入哪个实例,该怎么办?我们将其命名为nextToEat
。
function nextToEat (animals) {
const sortedByLeastEnergy = animals.sort((a,b) => {
return a.energy - b.energy
})
return sortedByLeastEnergy[0].name
}
由于我们不想在所有实例之间共享它,所以让它nextToEat
继续存在是没有意义的。相反,我们可以把它看作是一个辅助方法。那么,如果不应该让它继续存在,我们应该把它放在哪里呢?答案显而易见:我们可以把它放在与类相同的作用域内,然后在需要的时候像平常一样引用它。Animal.prototype
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 个方法的对象,constructor
分别是eat
、sleep
、 和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
为了与我们之前讨论的 相呼应Object.create
,它之所以有效,是因为 的任何实例在查找失败时Animal
都会委托给Animal.prototype
。因此,当您尝试访问 时leo.constructor
,leo
由于 没有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]}`)
}
你期望看到什么?很可能是这样的——
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
循环会遍历对象本身以及它委托给的原型上的所有可枚举属性name
。因为默认情况下,添加到函数原型的任何属性都是可枚举的,所以我们不仅可以看到和energy
,还可以看到原型上的所有方法 - eat
、sleep
和play
。为了解决这个问题,我们要么需要指定所有原型方法都是不可枚举的,要么需要一种方法,仅在属性位于对象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]}`)
}
}
现在我们看到的只是对象本身的属性,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
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
它的工作原理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 开发者,有时也会被上面的示例绊倒。因为我们使用了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
关键字 调用的,那么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
}
new
现在,如果我们重新调用该函数,但这次使用关键字,那么不仅仅是向函数的使用者记录警告,会怎么样?
function Animal (name, energy) {
if (this instanceof Animal === false) {
return new Animal(name, energy)
}
this.name = name
this.energy = energy
}
现在无论是否Animal
使用new
关键字调用,它仍然可以正常工作。
重新创建 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
关键字调用它时,我们知道返回的是一个对象,当查找失败时,它会委托给函数的原型。如果我们覆盖函数的原型,那么我们就可以决定在查找失败时委托给哪个对象。因此,在上面的例子中,我们Fn
用调用时传入的对象覆盖了 的原型,Object.create
我们称之为objToDelegateTo
。
请注意,我们仅支持 Object.create 的单个参数。官方实现还支持第二个可选参数,允许您为创建的对象添加更多属性。
箭头函数
箭头函数没有自己的this
关键字。因此,箭头函数不能作为构造函数,并且如果尝试使用new
关键字调用箭头函数,则会引发错误。
const Animal = () => {}
const leo = new Animal() // Error: Animal is not a constructor
另外,因为我们上面证明了伪经典模式不能与箭头函数一起使用,所以箭头函数也没有prototype
属性。
const Animal = () => {}
console.log(Animal.prototype) // undefined
这最初发表在TylerMcGinnis.com上,是其高级 JavaScript课程的一部分。
文章来源:https://dev.to/tylermcginnis/a-beginners-guide-to-javascripts-prototype-5kk