视频Video
https://www.youtube.com/watch...编程
若是很差好的学习对象,你就没法在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,咱们将函数自己称为“构造函数”,由于它负责“构造”一个新对象。ide
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)
如今,每当咱们想要创造一种新动物(或者更普遍地说是一种新的“实例”)时,咱们所要作的就是调用咱们的动物功能,将动物的名字和能量水平传递给它。这很是有效,并且很是简单。可是,你能发现这种模式的弱点吗?最大的和咱们试图解决的问题与三种方法有关 - 吃饭,睡觉和玩耍。这些方法中的每一种都不只是动态的,并且它们也是彻底通用的。这意味着没有理由从新建立这些方法,正如咱们在建立新动物时所作的那样。你能想到一个解决方案吗?若是不是每次建立新动物时从新建立这些方法,咱们将它们移动到本身的对象而后咱们可让每一个动物引用该对象,该怎么办?咱们能够将这种模式称为功能实例化与共享方法🤷♂️。函数
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容许您建立一个对象,只要该对象上的属性查找失败,它就能够查询另外一个对象以查看该另外一个对象是否具备该属性。咱们来看一些代码。oop
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)建立的,因此每当在子级上查找失败的属性时,JavaScript都会将该查找委托给父对象。这意味着即便孩子没有遗产,父母也会在记录时这样作。这样你就会获得父母的遗产(属性值的传递)。学习
如今在咱们的工具中使用Object.create,咱们如何使用它来简化以前的Animal代码?好吧,咱们可使用Object.create委托给animalMethods对象,而不是像咱们如今同样逐个将全部共享方法添加到动物中。听起来很聪明,让咱们将这个称为功能实例化与共享方法用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对象。this
到如今为止还挺好。尽管如此,咱们仍然能够作出一些改进。为了跨实例共享方法,必须管理一个单独的对象(animalMethods)彷佛有点“hacky”。这彷佛是您但愿在语言自己中实现的常见功能。这就是你在这里的所有缘由 - 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)
👏👏👏 一样,原型只是JavaScript中每一个函数都具备的属性,而且如上所述,它容许咱们在函数的全部实例之间共享方法。咱们全部的功能仍然相同,但如今咱们没必要为全部方法管理一个单独的对象,咱们可使用另外一个内置于Animal函数自己的对象Animal.prototype。
在这一点上,咱们知道三件事:
这三个任务彷佛是任何编程语言的基础。JavaScript是否真的那么糟糕,没有更简单“内置”的方式来完成一样的事情?然而并非的,它是经过使用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关键字调用一个函数时,这两行是隐式完成的(JavaScript引擎),而且建立的对象称为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
此模式的名称是Pseudoclassical Instantiation(原型实例化)。
若是JavaScript不是您的第一种编程语言,您可能会有点不安。
对于那些不熟悉的人,Class容许您为对象建立蓝图。而后,不管什么时候建立该类的实例,都会得到一个具备蓝图中定义的属性和方法的对象。
听起来有点熟?这基本上就是咱们对上面的Animal构造函数所作的。可是,咱们只使用常规的旧JavaScript函数来从新建立相同的功能,而不是使用class关键字。固然,它须要一些额外的工做以及一些关于JavaScript引擎运行的知识,但结果是同样的。
这是个好消息。JavaScript不是一种死语言。它正在不断获得改进,并由TC-39委员会添加。事实上,2015年,发布了EcmaScript(官方JavaScript规范)6(ES6),支持Classes和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 = []
事实证实,建立一个新的Array类其实也是一个语法糖。
const friendsWithSugar = [] const friendsWithoutSugar = new Array()
您可能从未想过的一件事是数组的每一个实例中的内置方法是从何而来的(splice, slice, pop, etc)?
正如您如今所知,这是由于这些方法存在于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等方法的缘由。
到目前为止,咱们已经介绍了为何以及如何在类的实例之间共享方法。可是,若是咱们有一个对Class很重要但不须要跨实例共享的方法呢?例如,若是咱们有一个函数接受一个Animal实例数组并肯定下一个须要接收哪个呢?咱们将其称为nextToEat。
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类相同的范围内,而后像咱们一般那样在须要时引用它。
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做为静态属性(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种方法,构造函数,吃饭,睡眠和游戏的对象。那讲得通。咱们在实例中使用了getPrototypeOf传递,leo获取了实例的原型,这是咱们全部方法都存在的地方。这告诉咱们关于原型的另一件事咱们尚未谈过。默认状况下,原型对象将具备构造函数属性,该属性指向原始函数或建立实例的类。这也意味着由于JavaScript默认在原型上放置构造函数属性,因此任何实例均可以经过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.prototype时,leo没有prototype属性,所以它会将该查找委托给Animal.prototype,它确实具备构造函数属性。若是这段没有理解到,请回过头来阅读上面的Object.create。
您可能已经看过 __ proto __以前用于获取实例的原型的方法,这已是过去式来,如上所述使用 Object.getPrototypeOf(instance)。
在某些状况下,您须要知道属性是否存在于实例自己上,仍是存在于对象委托的原型上。咱们能够经过循环咱们建立的leo对象来看到这一点。让咱们说目标是循环leo并记录它的全部键和值。使用for循环,可能看起来像这样。
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循环将循环遍历对象自己以及它所委托的原型的全部可枚举属性。由于默认状况下,您添加到函数原型的任何属性都是可枚举的,咱们不只会看到名称和能量,还会看到原型上的全部方法 - 吃,睡,玩。要解决这个问题,咱们须要指定全部原型方法都是不可枚举的或者咱们须要一种相似console.log的方法,若是属性是leo对象自己而不是leo委托给的原型在失败的查找。这是hasOwnProperty能够帮助咱们的地方。
hasOwnProperty是每一个对象上的一个属性,它返回一个布尔值,指示对象是否具备指定的属性做为其本身的属性,而不是对象委托给的原型。这正是咱们所须要的。如今有了这些新知识,咱们能够修改咱们的代码,以便利用in循环中的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
若是object是Class的实例,则上面的语句将返回true,不然返回false。回到咱们的动物示例,咱们会有相似的东西。
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 }
这彷佛是一个很是重要的细节,让其余开发人员记住。假设咱们正在与其余开发人员合做,有没有办法确保咱们的Animal构造函数始终使用new关键字调用?事实证实,它是经过使用咱们以前学到的instanceof运算符来实现的。
若是使用new关键字调用构造函数,那么构造函数体的内部将是构造函数自己的实例。这是一些代码。
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
}
如今不管是否使用new关键字调用Animal,它仍然能够正常工做。
从新建立Object.create
在这篇文章中,咱们很是依赖于Object.create来建立委托给构造函数原型的对象。此时,您应该知道如何在代码中使用Object.create,但您可能没有想到的一件事是Object.create其实是如何工做的。为了让你真正了解Object.create是如何工做的,咱们将本身从新建立它。首先,咱们对Object.create的工做原理了解多少?
它接受一个对象的参数。
它建立一个对象,该对象在失败的查找中委托给参数对象。
它返回新建立的对象。
让咱们从#1开始吧。
Object.create = function (objToDelegateTo) {
}
很简单。
如今#2 - 咱们须要建立一个对象,该对象将在失败的查找中委托给参数对象。这个有点棘手。为此,咱们将使用咱们对新关键字和原型如何在JavaScript中工做的知识。首先,在Object.create实现的主体内部,咱们将建立一个空函数。而后,咱们将该空函数的原型设置为等于参数对象。而后,为了建立一个新对象,咱们将使用new关键字调用咱们的空函数。若是咱们返回新建立的对象,那么它也将完成#3。
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
译者注:如下是一些扩展阅读,但愿对理解这篇文章有所帮助