An in-depth guide to JavaScript prototypes

Summary: Understand prototype.

Fundebug Copyright shall be owned by the original author when authorized to reproduce.

You can't go far in JavaScript without learning how to handle objects.They are the basis for almost every aspect of the JavaScript programming language.In fact, learning how to create objects may be the first thing you start learning.

The object is a key/value pair.The most common way to create an object is to use curly brackets {} and add attributes and methods to the object using point notation.

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
}

Now, in our app, we need to create multiple animals.The next step, of course, is to encapsulate logic. When we need to create a new animal, we simply call a function. We call this pattern unctional Instantiation of a function, and we call the function itself a constructor because it is responsible for "constructing" a new object.

Instantiation of a function

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)

Now, whenever we want to create a new animal (or, more broadly, a new "instance"), all we have to do is call our Animal function and pass in the parameters: name and energy.This is useful and very simple.But can you tell us about the drawbacks of this model?

The biggest problem we're trying to solve is related to the three methods within the function - eat, sleep, and play.Each of these methods is not only dynamic, but also fully universal.This means that there is no reason to recreate these methods when creating new animal s, as it is now.We're just wasting memory so that each new object is larger than it actually needs to be.

Can you think of a solution?What if we didn't recreate these methods every time we created a new animal, we moved them to our own objects, and then we could have each animal reference the object?We can call this pattern a function instantiation and sharing method.

Function Instantiation and Sharing Method

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)

By moving the sharing method to their own object and referencing it in the Animal function, we have now solved the problem of memory waste and the new object being too large.

Object.create

Let's use Object.create again to improve our example.Simply put, Object.create allows you to create an object that will be delegated to another object in a failed search.In other words, Object.create allows you to create an object that can query another object to see if it has the property if the property search on that object fails.Let's look at some code:

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

Therefore, in the example above, since the child was created with object.create(parent), JavaScript delegates the search to the parent object whenever a property lookup on the child object fails.This means that even if a child has no attribute heritage, when you print child.heritage, it will find the corresponding heritage from the parent object and print it out.

How do you use Object.create now to simplify the previous Animer code?Well, instead of adding all the sharing methods to animals one by one as we do now, we can use Object.create to delegate to animalMethods objects.For the sake of lattice B, this is called instantiation using shared methods and functions of Object.create.

Instantiate using shared methods and functions of 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)

So now when we call leo.eat, JavaScript will look for the eat method on the Leo object because
There is no eat method in leo, so the lookup will fail, because Object.create delegates it to the animalMethods object, so the eat method will be found on the animalMethods object.

It's good so far.Nevertheless, we can still make some improvements.In order to share methods across instances, having to manage a single object (animalMethods) seems a bit silly.We want this to be a common feature implemented in the language itself, so we need to introduce the next property, prototype.

So what is a prototype in JavaScript?Well, in short, every function in JavaScript has a prototype property that references an object.

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

What if instead of creating a separate object to manage our methods (animalMethods in the example above), we just put each method on the prototype of the Animal function?Then all we have to do is not delegate Object.create to animalMethods, which we can use to delegate Animal.prototype.We call this pattern prototype instantiation.

Prototype 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)

Similarly, prototype is just a property that every function in JavaScript has, and as we saw earlier, it allows us to share methods across all instances of a function.All our functions are still the same, but now we don't have to manage a single object for all methods, we just need to use another object, Animal.prototype, built into the Animal function itself.

BUGs that may exist after code deployment are not known in real time. In order to solve these BUGs afterwards, a lot of time has been spent debugging the log. By the way, a useful BUG monitoring tool is recommended. Fundebug.

Further more

Now we know three points:

  1. How to create a constructor.
  2. How to add a method to the prototype of a constructor.
  3. How to delegate failed lookups to the prototype of a function using Object.create.

These three points are fundamental to any programming language.Is JavaScript really so bad that there is no simpler way to do the same thing? As you might have guessed, it's now available by using the new keyword.

Looking back at our Animal constructor, the two most important parts are creating an object and returning it.If we don't use Object.create to create objects, we won't be able to delegate the prototype of the function on the failed lookup.If there is no return statement, we will never return the created object.

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

  return animal   // 2
}

One cool thing about new is that when you call a function using the new keyword, the following lines of code numbered 1 and 22 implicitly (at the bottom) complete for you, creating an object called this.

Use comments to show what's happening at the bottom level, and assume that the Animal constructor was called with the new keyword, so you can override it.

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)

Normally as follows:

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)

Again, this is done and this object was created for us because we called the constructor with the new keyword.If you omit new when calling a function, the object will never be created or returned implicitly.We can see this in the examples below.

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

const leo = Animal('Leo', 7)
console.log(leo) // undefined

This pattern is called pseudo class instantiation.

For those unfamiliar, classes allow you to create blueprints for objects.Then, whenever you create an instance of this class, you can access the properties and methods defined in this object.

Sounds a little familiar?This is basically what we did with the Animal constructor above.However, instead of using the class keyword, we only re-create the same functionality using the regular old JavaScript functions.Of course, it requires some extra work and some understanding of what happens "at the bottom" of JavaScript, but the results are the same.

That's good news.JavaScript is not a dead language.The TC-39 Committee is continuously improving and complementing.This means that even though the initial version of JavaScript did not support classes, there was no reason to add them to the official specification.In fact, that's exactly what TC-39 Committee Done.In 2015, EcmaScript (the official JavaScript specification) 6 was released, supporting class and class keywords.Let's see how the nimal constructor above uses the new class syntax.

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)

This is a relatively straightforward example.

So if this is a new way to create classes, why do we spend so much time reviewing the old ways?The reason is that the new method (using the class keyword) is primarily a "grammatical sugar" of what we call the pseudo-Class instantiation pattern.To fully understand the convenient syntax of the ES6 class, you must first understand the pseudo class instantiation pattern.

So far, we've introduced the basic principles of JavaScript prototypes.The rest of this article will focus on understanding other good topics related to it.In another article, we'll explore how to take advantage of these fundamental principles and use them to understand how inheritance works in JavaScript.

Array method

We discussed in depth how to share methods between instances of a class above, and you should prototype these methods on a class (or function).If we look at the Array class, we can see the same pattern.

const friends = []

Think it's a grammatical sugar instead of using new Array().

const friendsWithSugar = []

const friendsWithoutSugar = new Array()

One thing you might never have thought about is how each instance of an array has all the built-in methods (splice, slice, pop, and so on)?

As you now know, this is because these methods exist on Array.prototype, and when you create a new Array instance, you use the new keyword to set the delegate to Array.prototype in the failed lookup.

We can print Array.prototype to see what methods are available:

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 also have exactly the same logic.All objects are delegated to Object.prototype after a failed lookup, which is why all objects have methods such as toString and hasOwnProperty

Static method

So far, we have discussed why and how to share methods between instances of classes.But what if we have a method that is important to a class but doesn't need to share it between instances? For example, if we have a function that takes in a series of instances of Animal and determines which one to feed next? We call this method nextToEat.

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

  return sortedByLeastEnergy[0].name
}

Since we don't want to share nextToEat between all instances, it doesn't make sense to use nextToEat on Animal.prototype.Instead, we can think of it as an auxiliary method.

So if nextToEat should not exist in Animal.prototype, where should we put it?The obvious answer is that we can place nextToEat in the same scope as our Animal class and then reference it as we normally would.

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

This is possible, but there is a better way.

As long as there is a method specific to the class itself that does not need to be shared between instances of the class, it can be defined as a static property of the class.

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
  }
}

Now, because we've added nextToEat as a static property to the class, it exists on the Animal class itself (not its prototype) and can be invoked using Animal.nextToEat.

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

console.log(Animal.nextToEat([leo, snoop])) // Leo

Because we've followed a similar pattern in this article, let's see how we can use ES5 to do the same thing.In the example above, we see how to use the static keyword to place a method directly on the class itself.With ES5, the same pattern is as simple as adding a method to a function object manually.

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

Get the prototype of the object

Whatever mode you use to create an object, you can use the Object.getPrototypeOf method to complete the prototype of the object.

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 proto  = Object.getPrototypeOf(leo)

console.log(proto )
// {constructor: ƒ, eat: ƒ, sleep: ƒ, play: ƒ}

proto === Animal.prototype // true

The code above has two important points.

First, you'll notice that proto is an object with four methods, constructor, eat, sleep, and play.This makes sense.We use getPrototypeOf to pass the instance, and leo retrieves the instance prototype, which is where we all work.

This tells us another thing about prototype that we haven't discussed yet.By default, the prototype object will have a constructor property that points to the initial function or class that created the instance.This also means that any instance can pass because JavaScript places constructor properties on the prototype by default.

The second important point is: Object.getPrototypeOf(leo) === Animal.prototype.That makes sense.The Animal constructor has a prototype property that allows us to share methods between all instances, and getPrototypeOf allows us to view the prototype of the instance itself.

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

const leo = new Animal('Leo', 7)
console.log(leo.constructor) // Logs the constructor function

To complement what we discussed earlier with Object.create, it works because any Animal instance is delegated to the Animal.prototype in a failed lookup.Therefore, when you try to access leo.constructor, Leo does not have a constructor property, so it delegates the lookup to Animal.prototype, which does have a constructor property.

You may have seen the proto type used to get instances before, which is a relic of the past.Instead, use Object.getPrototypeOf(instance) as described above

Determine whether a prototype contains an attribute

In some cases, you need to know whether attributes exist on the instance itself or on the prototype of an object delegate.We can see this by printing the leo objects we created in a loop.Use the for in loop as follows:

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

The results I expect to print may be as follows:

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

However, if you run the code, you see that -

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
}

Why is that?for in loops, loops iterate through all enumerable properties of the object itself and the prototype it delegates.Because by default, any properties you add to a function prototype are enumerable, we will not only see name and energy, but also all the methods on the prototype - eat, sleep, play.

To solve this problem, we need to specify that all prototype methods are non-enumerable, or that only print attributes are on the leo object itself and not on the prototype that leo delegates to the failed lookup.This is where hasOwnProperty can help us.

...

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

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

What we see now is the properties of the leo object itself, not the prototype of the leo delegation.

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

If you're still confused about hasOwnProperty, here's some code to help you figure it out better.

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

Check if the object is an instance of a class?

Sometimes you want to know if an object is an instance of a particular class.To do this, you can use the instanceof operator.The use case is very simple, but if you've never seen it before, the actual syntax is a bit strange.It works as follows

object instanceof Class

If the object is an instance of Class, the statement above returns true, otherwise it returns false.Back in our Animal example, we have something similar:

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 works by checking the existence of constructor.prototype in the object prototype chain.In the example above, leo instanceof Animal is true because Object.getPrototypeOf(leo) === Animal.prototype.In addition, leo instanceof User is false because Object.getPrototypeOf(leo)!== User.prototype.

Create a new unknown constructor

Can you find the error in the code below

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

const leo = Animal('Leo', 7)

Even experienced JavaScript developers sometimes get stuck with the above examples.Since we're using the pseudo class instance pattern we've learned earlier, when calling the Animal constructor, we need to make sure it's called using the new keyword.If we don't do this, the this keyword will not be created, nor will it be returned implicitly.

As a review, the commented out lines are what happens behind the use of the new keyword in a function.

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

  this.name = name
  this.energy = energy

  // return this
}

For other developers to remember, this seems to be a very important detail.Assuming we're working with other developers, can we ensure that our Animal constructor is always called using the new keyword?It turns out that this can be achieved by using the instanceof operator we learned earlier.

If the constructor is called with the new keyword, the inner this of the constructor body will be an instance of the constructor itself.

function Aniam (name, energy) {
  if (this instanceof Animal === false) {
     console.warn('Forgot to call Animal with the new keyword')
  }

  this.name = name
  this.energy = energy
}

Now, if we call the function again, but this time use the new keyword instead of just printing a warning to the caller of the function?

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

  this.name = name
  this.energy = energy
}

Animal now works regardless of whether or not it is called with the new keyword.

Override Object.create

In this article, we rely heavily on Object.create to create objects that are delegated to the prototype of the constructor.At this point, you should know how to use Object.create in your code, but one thing you might not think about is how Object.create actually works.To give you a real idea of how Object.create works, we'll recreate it ourselves.First, how much do we know about how Object.create works?

  1. It accepts the parameters of an object.
  2. It creates an object that is delegated to a parameter object when the lookup fails
  3. It returns the newly created object.
Object.create = function (objToDelegateTo) {

}

Now we need to create an object that will be delegated to the parameter object in the failed lookup.This is a bit tricky.To do this, we'll use the knowledge associated with the new keyword.

First, create an empty function inside the Object.create body.Then, set the prototype of the empty function to be equal to the incoming parameter object.Then, return to calling our empty function with the new keyword.

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

When we create a new function Fn in the code above, it has a prototype attribute.When we call it with the new keyword, we know what we're going to get is an object that will be delegated to the function prototype in the failed lookup.

If we override the prototype of a function, we can decide which object to delegate in a failed lookup.So in the example above, we override the Fn prototype with the object we passed in when we called Object.create, which we call objToDelegateTo.

Note that we only support a single parameter for Object.create.The official implementation also supports a second optional parameter that allows you to add more attributes to the object you create.

Arrow function

The arrow function does not have its own this keyword.Therefore, an arrow function cannot be a constructor, and if you try to call the arrow function using the new keyword, it will raise an error.

const Animal = () => {}

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

Also, since we have shown above that pseudo class instance patterns cannot be used with arrow functions, arrow functions do not have prototype properties.

const Animal = () => {}
console.log(Animal.prototype) // undefined

About Fundebug

Fundebug Focus on JavaScript, WeChat applets, WeChat games, Alipay applets, React Native, Node.js, and real-time BUG monitoring for Java online applications.Since the official launch of November 11, 2016, Fundebug has handled a cumulative billion + error events. Payment customers include Google, 360, Jinshan Software, People's Network and many other brand enterprises.Welcome Free Trial!

Keywords: Javascript Attribute Programming ECMAScript

Added by webzyne on Thu, 16 May 2019 03:41:26 +0300