paint-brush
A Definitive Guide to JavaScript Prototypesby@kevinze
11,098 reads
11,098 reads

A Definitive Guide to JavaScript Prototypes

by Kevin LeeJanuary 15th, 2018
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

With bite-size code snippets and easy explanations.

Coin Mentioned

Mention Thumbnail
featured image - A Definitive Guide to JavaScript Prototypes
Kevin Lee HackerNoon profile picture

With bite-size code snippets and easy explanations.

Introduction

JavaScript prototypes are confusing and unfamiliar to many developers and engineers.

Today, it’s time to demystify and master prototypes once and for all. Doing so will give us the confidence to deal with prototypes when we inevitably encounter and use them in JavaScript.

Sections

This guide is divided into the following sections.

  • Effects of creating a function
  • Invoking a function as a constructor
  • Demonstration of prototypal inheritance
  • Traversal of the prototype chain
  • Components of the prototype chain
  • A function’s prototype
  • Prototypes without constructors
  • Why is a prototype called a prototype?

Tips on how to read this guide

Each section builds on the previous one. As such, don’t skip sections, especially if you are reading this guide for the first time.

All code snippets are relevant and are cumulative across sections. Code snippets should work well with recent versions of JavaScript.

If you scroll through this guide too quickly, you may not gain much from it. Try to read this guide carefully and you will likely become convinced of what prototypes are and how to use them.

It’s time to demystify and master prototypes once and for all.

Effects of creating a function

Creating a function has two effects.


  1. The function itself will be created.Note that a function is also an object and thus it can have additional properties.

  2. A second object will be created and become attached to the function as <Function>.prototype.

To illustrate these effects, create a function named Person and observe that it automatically comes with a Person.prototype object.



function Person(name) {this.name = name;}

typeof Person.prototype; // "object"

Invoking a function as a constructor

When we invoke the Person function with the new keyword, i.e. as a constructor, a new object this is implicitly created, this.name is set, and finally, this is implicitly returned.



function Person(name) {this.name = name;}



const alex = new Person("Alex");typeof alex; // "object"alex.name; // "Alex"

Demonstration of prototypal inheritance

Importantly, the object alex and any other object constructed from Person will gain indirect access to Person.prototype.

Let’s add a greet function to Person.prototype. Notice that the existing object alex can now greet, and a newly created object tom can do the same. This form of code reuse is known as prototypal inheritance.



Person.prototype.greet = function() {console.log(`Hi ${this.name}`);}


alex.hasOwnProperty("greet"); // falsealex.greet(); // "Hi Alex"


const tom = new Person("Tom");tom.greet(); // "Hi Tom"

Even though the object alex constructed from Person does not have a greet property on itself, it was able to access Person.prototype and thus invoke Person.prototype.greet with this being implicitly set to alex, which results in “Hi Alex” being logged to the console.

The other object tom gains access to the same Person.prototype object in a similar way.

Traversal of the prototype chain

The traversal algorithm consults the object’s prototype when it cannot find the desired property on the object. If it finds the property on the prototype, the traversal stops. Otherwise, it will consult the prototype of the prototype, and so on, until it finds the property or it reaches the end of the prototype chain.

At each traversal, the object delegates the responsibility of doing something to its prototype if it does not know how to do it. alex did not know how to greet, so alex asked Person.prototype for help on how to greet.

Components of the prototype chain

Let’s use Object.getPrototypeOf to check alex’s entire prototype chain.

Object.getPrototypeOf(alex) === Person.prototype; // true

The line above tells us that the prototype of alex is Person.prototype. Therefore, if JavaScript cannot find the desired property on alex, it will check Person.prototpe.

In other words, alex’s prototype chain starts with Person.prototype.

If JavaScript is still unable to find the desired property on Person.prototype, it will look at the prototype of Person.prototype, which is Object.prototype.

Object.getPrototypeOf(Person.prototype) === Object.prototype; // true

Why is Object.prototype the prototype of Person.prototype?

Suppose that Person.prototype, which is an object, was constructed from the built-in Object constructor (whether or not this is the case is an implementation detail).

You can observe a pattern consistent with what we have learned so far.


  1. alex was constructed from Person.The prototype of alex is Person.prototype.


  2. Person.prototype was constructed from Object.The prototype of Person.prototype is Object.prototype.

You can go from statement 1 to 2 by first substituting Person with Object, and then substituting alex with Person.prototype.

To recap, we have seen that the prototype of alex is Person.prototype, and the prototype of Person.prototype is Object.prototype. Therefore, alex’s prototype chain contains Person.prototype followed by Object.prototype.

Is Object.prototype the final prototype in alex’s prototype chain? Yes, because Object.prototype does not have a prototype (it is null).

Object.getPrototypeOf(Object.prototype) === null; // true

Even though Object.prototype is an object, its prototype is not Object.prototype, otherwise we will have an infinite prototype chain.

Nearly all other objects in JavaScript have Object.prototype at the end of their prototype chains. We have seen how it is so for the object alex which was constructed from Person. This property also applies to plain objects created from the built-in Object constructor and the object literal syntax.


const constructedObject = new Object();const objectLiteral = {};


Object.getPrototypeOf(constructedObject) === Object.prototype; // trueObject.getPrototypeOf(objectLiteral) === Object.prototype; // true

The fact that nearly all objects have Object.prototype at the end of their prototype chains is of practical significance because they will have access to common utilities offered by Object.prototype such as toString and valueOf.


alex.toString(); // "[object Object]"alex.valueOf(); // Person { name: "Alex" }

A function’s prototype

Earlier, we saw that alex has a name property which can be accessed using alex.name. The syntax does not say anything more about alex, but we usually add more meaning to it. We think that alex.name is not just any random name that happens to be accessible at alex.name, but instead refers to Alex’s name.

What about Person.prototype? Does it refer to Person’s prototype?

Nope!

Object.getPrototypeOf(Person) !== Person.prototype; // true

If Person.prototype does not refer to Person’s prototype, then whose prototype does it refer to?

Well, Person.prototype will become the prototype of objects constructed from Person. We have already seen this behavior with alex.

It may help to think of Person.prototype as a gift that Santa deposited at your house, but that gift is meant for your kids and is not yours.

So what is the actual prototype of Person? It is Function.prototype.

Object.getPrototypeOf(Person) === Function.prototype; // true

This is because Person is a Function and thus it has a prototype of Function.prototype.

Function.prototype offers common utilities like call, bind and apply which can be accessed from Person and other functions.

Prototypes without constructors

We can also create prototype chains without constructors.





const greeter = {greet() {console.log(`Hi ${this.name}`);}};



const bob = Object.create(greeter);bob.name = "Bob";bob.greet(); // "Hi Bob"

Object.getPrototypeOf(bob) === greeter; // true

In the above example, Object.create created a new object with greeter as its prototype. That object was then assigned to bob. Although bob does not have its own greet function, it is able to access the greet function on its prototype greeter.

Creating a new object with a defined prototype using Object.create is more straightforward than having to deal with a constructor function and the <Constructor>.prototype object.

The use of Object.create can be combined with factory functions. Unlike constructor functions, factory functions explicitly return an object and are not invoked with new. Here is an example.





function createPerson(name, prototype) {const person = Object.create(prototype);person.name = name;return person;}


const ada = createPerson("Ada", greeter);ada.greet(); // "Hi Ada"

Given their simplicity and conciseness, the use of factory functions and Object.create tends to be the preferred approach over the use of constructor functions and the <Constructor>.prototype objects.

Why is a prototype called a prototype?

“Because someone came up with it” is not a satisfactory answer.

The word prototype is often glossed over in JavaScript literature. Most people treat it as a technical term and do not explain why it is suitably named “prototype”. Knowing why a prototype is called a prototype can give us a better mental model to work with whenever we encounter it.

That being said, it is hard to find an exact answer to this question. Here is my take on this question.

A prototype in real life mainly refers to

  1. A product,
  2. albeit with limited features, where
  3. the final version of a product will share some characteristics of its prototype.

After substituting the word “product” with “object”, we see that a JavaScript prototype refers to

  1. An object,
  2. albeit with limited features, where
  3. the final version of an object will share some characteristics of its prototype.

Point 1 highlights a distinct point about prototypal inheritance as compared to traditional class-based inheritance. The prototype is an object that can be used on its own. However, a traditional class is not an object and thus cannot be used like an object.

Note that although later versions of JavaScript have a class keyword, it still uses prototypal inheritance under the hood.

Point 2 is valid because a prototype typically has fewer properties compared to an object that uses it as its prototype. For example, both alex and Person.prototype are able to greet (although alex does it with the help of Person.prototype), but alex has an additional name.

As for point 3, a JavaScript object does share some characteristics of its prototype(s) because of prototypal inheritance.

Since objects are linked in the prototype chain, if you change a prototype, the behavior of existing and future objects linked to that prototype could be affected.

As such, be careful not to change a built-in prototype unless you are trying to polyfill a standard feature. If everyone took the liberty of changing built-in prototypes arbitrarily, there will be conflicts and broken code.

Summary

You have seen what prototypes are, how JavaScript traverses the prototype chain to access prototype properties, and thus how code is reused in what we call prototypal inheritance. You have also seen how to use prototypes, with or without constructor functions.

Though the mastery of prototypes has eluded many, I hope that this definitive guide has helped you to master prototypes in JavaScript and thus become a better software developer and engineer.

Congratulations for making it all the way here!

If you find this guide useful, send it to your colleagues and friends who may benefit from it.

For further reading

  1. Common Misconceptions About Inheritance in JavaScript by Eric Elliott
  2. You Don’t Know JS: this & Object Prototypes by Kyle Simpson