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) web前端1-3年 进阶Q君羊:731771211 最前沿技术免费分享
“我认为这是高级JavaScript......?”
现在,每当我们想要创造一种新动物(或者更广泛地说是一种新的“实例”)时,我们所要做的就是调用我们的动物功能,将动物的名字和能量水平传递给它。这非常有效,而且非常简单。但是,你能发现这种模式的弱点吗?最大的和我们试图解决的问题与三种方法有关 - 吃饭,睡觉和玩耍。这些方法中的每一种都不仅是动态的,而且它们也是完全通用的。这意味着没有理由重新创建这些方法,正如我们在创建新动物时所做的那样。你能想到一个解决方案吗?如果不是每次创建新动物时重新创建这些方法,我们将它们移动到自己的对象然后我们可以让每个动物引用该对象,该怎么办?我们可以将这种模式称为功能实例化与共享方法
使用共享方法的功能实例化
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)创建的,所以每当在子级上查找失败的属性时,JavaScript都会将该查找委托给父对象。这意味着即使孩子没有遗产,父母也会在你记录孩子时这样做。这样你就会得到父母的遗产(属性值的传递)。
现在在我们的工具棚中使用Object.create,我们如何使用它来简化之前的Animal代码?好吧,我们可以使用Object.create委托给animalMethods对象,而不是像我们现在一样逐个将所有共享方法添加到动物中。
听起来很聪明,让我们将这个称为功能实例化与共享方法用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) web前端1-3年 进阶Q君羊:731771211 最前沿技术免费分享
所以现在当我们调用leo.eat时,JavaScript会在leo对象上查找eat方法。那个查找将失败,因为Object.create,它将委托给animalMethods对象。
到现在为止还挺好。尽管如此,我们仍然可以做出一些改进。为了跨实例共享方法,必须管理一个单独的对象(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。
在这一点上,我们知道三件事:
1.如何创建构造函数。
2.如何将方法添加到构造函数的原型中。
3.如何使用Object.create将失败的查找委托给函数的原型。
这三个任务似乎是任何编程语言的基础。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(原型实例化)。
对于那些不熟悉的人,Class允许您为对象创建蓝图。然后,无论何时创建该类的实例,都会获得一个具有蓝图中定义的属性和方法的对象。
听起来有点熟?这基本上就是我们对上面的Animal构造函数所做的。但是,我们只使用常规的旧JavaScript函数来重新创建相同的功能,而不是使用class关键字。当然,它需要一些额外的工作以及一些关于JavaScript引擎运行的知识,但结果是一样的。
JavaScript不是一种死语言。它正在不断得到改进
看看上面的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类的便捷语法,首先必须理解伪古典模式。
数组方法
我们在上面深入讨论了如果要在类的实例之间共享方法,您应该将这些方法放在类(或函数)原型上。如果我们查看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() */ web前端1-3年 进阶Q君羊:731771211 最前沿技术免费分享
对象也存在完全相同的逻辑。所有对象将在失败的查找中委托给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
确定属性是否存在于原型上
在某些情况下,需要知道属性是否存在于实例本身上,还是存在于对象委托的原型上。我们可以通过循环我们创建的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: 7Key: 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 } web前端1-3年 进阶Q君羊:731771211 最前沿技术免费分享
这是为什么?for循环将循环遍历对象本身以及它所委托的原型的所有可枚举属性。因为默认情况下,你添加到函数原型的任何属性都是可枚举的,我们不仅会看到名称和能量,还会看到原型上的所有方法 - 吃,睡,玩。要解决这个问题,我们需要指定所有原型方法都是不可枚举的或者我们需要一种类似console.log的方法,如果属性是leo对象本身而不是leo委托给的原型在失败的查找。这是hasOwnProperty可以帮助我们的地方。
这是为什么?for循环将循环遍历对象本身以及它所委托的原型的所有可枚举属性。因为默认情况下,您添加到函数原型的任何属性都是可枚举的,我们不仅会看到名称和能量,还会看到原型上的所有方法 - 吃,睡,玩。要解决这个问题,我们需要指定所有原型方法都是不可枚举的或者我们需要一种类似console.log的方法,如果属性是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
如果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。
箭头函数
箭头函数没有自己的this关键字。因此,箭头函数不能是构造函数,如果您尝试使用new关键字调用箭头函数,它将抛出错误。
const Animal = () => {} const leo = new Animal() // Error: Animal is not a constructor web前端1-3年 进阶Q君羊:731771211 最前沿技术免费分享
另外,为了证明箭头函数不能是构造函数,如下,我们看到箭头函数也没有原型属性。
const Animal = () => {} console.log(Animal.prototype) // undefined
- 通俗的英文指南——Javascript的原型
- javascript 小白学习指南 理解隐形原型
- JavaScript中的prototype原型学习指南
- 深入理解javascript原型和闭包(5)——instanceof
- 深入理解javascript原型和闭包(10)——this
- 深入理解javascript原型和闭包(11)——执行上下文栈
- 关于javascript 原型(prototype) 的实验和结论
- 深入理解javascript原型和闭包(15)——闭包
- 最详细的JavaScript事件使用指南
- Javascript:必须知道的Javascript知识点之“原型链”
- 【JavaScript】面向对象与原型
- javascript正则表达式参数/g与/i及/gi的使用指南
- javascript混合构造函数/原型方式定义类
- JavaScript之原型prototype
- 让javascript显原型!
- JavaScript 原型继承之构造函数继承
- Javascript中的对象和原型(一)
- JavaScript中的原型prototype属性使用详解
- JavaScript中创建对象的方法:工厂模式,构造函数模式, 原型模式
- javascript原型继承(1)