Back

JavaScript Fundamentals - Prototypes

As I continue my self-study journey to learn JavaScript at a foundtaional level, the topic of Prototypes has come up a lot. This post is my attempt at explaining it to myself (and others!) The goal of this post is to practice my “public-learning”. Feedback from others is among the benefits of learning in public, so let me know what you think!

Intro to Objects

Feel free to skip this if you know what a JavaScript object is.

To get started, let’s create an object. An object is simple a pair of key and value pairs. Like this:

var movie = {
  title: 'Toy Story',
  releaseYear: '1995',
}

To look up the value, we type something like this:

movie.title // output: Toy Story
movie[title] // output: Toy Story

We can also add properties to an object, like this:

movie.rottenTomatoesScore = 100

Now, if we output the object, it will look like this:

{
	"title": "Toy Story",
	"releaseYear": "1995",
	"rottenTomatoesScore": 100
}

objects can have simple properties like strings or numbers, but they can also have actions a.k.a methods a.k.a functions.

movie.play = function() {
  return 'Playing ' + this.title
}

Then to run the method we type:

movie.play() // output: Playing Toy Story

Prototypes with Arrays

Let’s see how prototypes works with a special object called the Array.

Think of Array like a list of values. We would create an array like this:

var movieList = ['Toy Story', 'Finding Nemo', 'Wall-e']
// or
var movieList = new Array('Toy Story', 'Finding Nemo', 'Wall-e')

And then to output one of the values of the list:

movieList[0] // output: Toy Story

Where 0 is the key and “Toy Story” is the value. A key-value pair!

The movieList is actually a special type of object! Remember how we added properties to objects before. We can do that with Arrays too because they’re just objects!

movieList.studio = 'Pixar'

This is what movieList looks like now: C334879C 5701 4524 8ABE 1BE92F780C1D

In JavaScript, when you try to look up a key on an object, and the key doesn’t exist on the current object, JavaScript will look up to the prototype to see if the prototype has the key.

The prototype is just another object! The movieList has something called the prototype that points to another object.

The movieList is basically saying something like this “If you can’t find what you’re looking for in my keys, go look at my prototype to see if you can find it there!”

So where did the prototype come from? We didn’t create it, so how did it show up?

Magic!

But really, whenever you create an Array like the movieList we created, JavaScript gives it a prototype that points to the Array.prototype, which is a built-in prototype in all JavaScript environments.

Let’s look at an example to see how delegating to the prototype works.

This is what the movieList object looks like after we create it BCF587AC 6A49 4106 B7B2 E0FBA8E3B9BA

See that last line <prototype>: Array []? That’s the prototype for the movieList array! We can lookup what the prototype like this:

Object.getPrototypeOf(movieList)

FE96F95C 97A1 468D 9FDB 8835BA44DB99

That’s called the Array.prototype and we can use any of those methods on the movieList array that we created. The movieList array is called an “instance” of the Array.prototype, and it inherits all of the methods of the Array.prototype. When you use the new keyword you’re creating an instance of the object.

So we can call something like this, which will combine arrays:

movieList.concat(['Monsters Inc.'])

Here’s my goofy dialogue for what’s going on with the above code:

Me: Hey, JS, could you call the function concat on the movieList object with this argument: [‘Mosnters Inc']

JS: Sure thing!

[JS walks over to movieList]

JS: Waddup movieList! Do you have a method called concat?

movieList: Hmm… nope! Go ask my prototype Array.prototype, they might have it.

[JS walks over to Array.prototype]

JS: Hey Array.prototype do you have a method called toString?

Array.prototype: Sure do! Here you go!

JS: Thanks, Array.prototype!

[JS uses the concat method with the values in movieList and the argument ['Monsters Inc.']]

[JS walks back over to me]

JS: Here’s your new Array: ["Toy Story", "Finding Nemo", "Wall-e", "Monsters Inc."]

Me: Gee, thanks JS! You’re so resourceful. Even when you first don’t find the function, you went to the prototype to see if they had it. You’re hired!

Here’s an illustration on how JS looks for the concat function:

Now that we have the concept down, let’s have some fun with it.

To illustrate that we’re using the Array.prototype.concat method, I’m going to modify what that method does.

movieList.concat = function() {
  return 'concat overwritten!'
}

Now our movieList array object has a new property called concat 2018 10 08 12 06

Now, when we call it, we get a different output!

movieList.concat(['Monsters Inc.']) // output: "concat overwritten!"

That’s because JavaScript found the object in movieList - it didn’t have to go to Array.prototype to look for it.

Ok, ready for some more fun?

If each Array inherits from the Array.prototype so whenever we call a method that isn’t on the current object, then the method from Array.prototype gets used, could we modify the methods from Array.prototype to get a different output for ALL of the Arrays?

Let’s try it out!

Danger: Don’t do this in your code. It will break all of the things (as you’ll see). I’m doing this for the sake of education.

Array.prototype.toString = function() {
  return 'Haxored all of the Arrays!'
}

Now, if we call it again:

movieList.toString() // output: Haxored all of the Arrays!
var numbers = [1, 2, 3]
numbers.toString() // output: Haxored all of the Arrays!

We just broke a method on the Array.prototype! It even broke it for arrays we didn’t create yet! That’s because arrays always point to Array.prototype which is just an object with methods sitting in the global scope. So if we update that object, all of the objects linked to it will inherit the updated method. The future arrays that we create will point to that same object!

What’s important to remember is this:

Objects are not copied in the prototype chain. They are linked. The Array.prototype stays the same, and all Arrays point to the same Array.prototype

Side note: modifying one of the built-in prototypes like Array.prototype is called monkey patching, and it’s not a good idea.

There’s an experimental feature called __proto__ (pronounced “dunder proto”) that allows you to get and set the prototype of an object. It’s basically a key that’s added to the object that points to the object’s prototype. It’s deprecated and not recommended by MDN but I’m going to use it for educational purposes. It still works in most browser, but don’t use it.

Since __proto__ is just a key, we can change where it points:

movieList.__proto__ = { studio: 'Pixar' }

A3FEC82F EF75 4095 9DD9 313DA36E17BE

4F548D89 8DD0 45E5 B8A1 B851296162B6

Now the prototype for our movieList array is a completely different object. The pop method doesn’t exist anymore because it doesn’t use the Array.prototype anymore.

Let’s undo what we did so we can access the Array.prototype methods again:

movieList.__proto__ = Array.prototype
// or
Object.setPrototypeOf(movieList, Array.prototype)

3BA9F0E8 A3DB 4AC9 8171 C20036BCFBE8

Using our new prototype powers for debugging

Now that we understand prototypical inheritance, let’s look at a practical example of why this knowledge is so helpful.

Let’s say we want to select all of the paragraphs on a web page:

var paragraphs = document.querySelectorAll('p')

Now, we want to find the paragraphs that have the word “Found” in it:

var foundParagraphs = paragraphs.filter(p => p.innerHTML.includes("Found"));

36B1935B EF5D 42DD 8A07 8E9FDAF2E1F2

Oh no! Why didn’t that work? I selected a bunch of elements, so I should be able to filter through them, right?

Let’s debug!

The error says that paragraph.filter is not a function, so let’s look at the paragraph’s prototype.

2018 10 08 11 14

So the querySelectorAll function created a list of the Nodes and used the NodeList Prototype instead of the Array prototype (this is the default behavior, by the way).

The NodeList prototype doesn’t have a filter function, so that’s why we got the error!

It might be tempting to just change the prototype of the paragraphs object, but this is a bad idea! Not only would this not work because filter expects an Array not a NodeList object and it would also de-optimize the JavaScript engine. See this blog post about the performance implications of changing an object’s prototype.

Instead, we should create a new object from the paragraphs object that uses the Array prototype, which has the filter function that we’re looking for.

var paragraphsArray = Array.from(paragraphs)

Note: there are many many ways to convert a NodeList to an Array, this is just one of them.

Let’s see what the prototype is now.

2018 10 08 11 24

Now we can run the filter on the paragraphsArray

var filteredParagraphs = paragraphsArray.filter(p =>
  p.innerHTML.includes('Found')
)

492DCD31 828D 4C53 9F77 2BDABBA2C0FB

Yay it worked!

In Summary

  • Every object in JavaScript has a prototype
  • Prototype points to the parent object
  • If an object doesn’t have a certain key, it looks to the prototype
  • The prototype chain is a way for objects to share properties and methods
  • Objects are not copied, they are linked.

Further Reading

I highly recommend Tyler McGinnis’ explanation of prototypes: A Beginner’s Guide to JavaScript’s Prototype. He goes into depth on how to create prototypes.

For performance optimizations with prototype method look ups, check out JavaScript engine fundamentals: optimizing prototypes · Mathias Bynens. The key point to remember from that article: don’t modify prototypes! I modified prototypes in this post simply to demonstrate how they work, not as a best practice scenario.

Questions? Comments? Let me know!

Published 6 Oct 2018

Front-end web developer. Civic tech hacker. San Francisco Bay Area.
Jeff Appareti on Twitter