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!
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
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:
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
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)
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
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' }
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)
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"));
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.
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.
Now we can run the filter on the paragraphsArray
var filteredParagraphs = paragraphsArray.filter(p =>
p.innerHTML.includes('Found')
)
Yay it worked!
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!