What I Learned at Work this Week: JavaScript Prototype

Photo by Kaboompics .com from Pexels

In my last blog post, I glossed over the JavaScript prototype, noting that I didn’t want to get sidetracked with a long explanation. This happened to be a short week at work, so I figured I’d come back to the topic and give it the attention that it deserves. Okay, it probably still deserves more attention than I’m about to give it, but I’ll do my best

The JavaScript prototype is directly linked to how the language handles inheritance. To get it out of the way: prototype is a property of a function that contains other properties that can be referenced, or inherited, by constructed objects or functions. For example, when we create an array in JS, we can call .length even though we haven’t defined it:

var ourArr = [1,2,3];
ourArr.length; // => 3

We haven’t defined length, but it is defined in JavaScript’s Array function. Try this in your console at home and you’ll see length contained in a list of properties, along with lots of other familiar functions like indexOf, map, and reduce:

Array.prototype; // => [constructor: ƒ, concat: ƒ, copyWithin: ƒ, fill: ƒ, find: ƒ, …]

If we want to put it simply, we could say that ourArr inherits the length property from Array. Want to confirm it? There’s a function for that!

Array.prototype.isPrototypeOf(ourArr); // => true

This is so unbelievably simple that I am now questioning why it’s taken me so long to research and write this blog post. The good news is that, if you’ve come this far, you have a pretty good working understanding of prototype. But it’s a subject that’s very popular in JS conversations (and possibly interviews as well), so read on if you’d like to learn more. Here are some of the questions I found myself asking as I read up on the subject.

This one was very tough for me. If you’re an excellent JavaScript student and unhappy with my simplified explanation, I’d suggest checking out Eric Elliott’s post here (though if you’re that into JS you’ve probably read it already). Here’s what I was able to take away:

  1. Prototypal inheritance (that’s what JS uses) allows us to be more selective with which properties we inherit. You may have heard of Joe Armstrong’s “Gorilla-Banana Problem” which essentially states that, when we use classical inheritance, our instances end up including a lot of stuff we don’t need. They might even contain something we want to replace, and that can create a challenge when we’re writing our code. In JavaScript, we can use object composition to pick and choose our object’s properties. To learn more about that, check out the MDN documentation on Object.assign.
  2. Delegation builds connections between functions rather than instances. If you’re like me, reading that last point may have you thinking “we didn’t get to pick and choose our properties when we created an array. We just got everything that was in the Array function!” And you’d be right — JavaScript gives us the power to build custom objects, but it more frequently uses delegation, which looks a lot more like classical inheritance. When we attempt to reference a property, the language will first check to see if that property is associated with the object or function in question.
ourArr.length; // => 3
// we didn’t define length for ourArr, so JS isn’t going to find anything when checking that specific object

If the property doesn’t exist, JavaScript will look to the delegate prototype. In other words, the function that is the prototype of our target. Since we know that Array is a prototype of ourArray, JavaScript is going to look there for the length property.

Array.prototype.length;
// it’s there, as we saw. So JavaScript will use this property to return a value.

If we’re referencing something that isn’t contained in the direct delegate prototype, JavaScript will look to the delegate prototype of the delegate prototype. This will continue until we get to the very top of our delegation, which is Object. Object’s delegate prototype is null, which of course doesn’t contain any properties. For example, Array.prototype does not contain the property toString, but we can call it on ourArr:

ourArr.toString(); // => “1,2,3”

That’s because toString is part of the Object prototype. Practically speaking, this isn’t too different than classical inheritance. Does it really matter where the property comes from? At the very least, I’ll say that since JavaScript creates functions instead of instances, it can be more dynamic. Our functions and objects are constantly referencing delegate prototypes to define their properties. So if something changes about that prototype, everything that uses it as a reference point will be immediately updated.

I find this all very difficult to understand without examples, so let’s see what we can write out. We’ve got two concepts here, the first of which is compositional inheritance. I’ve now seen several different sources that mention compositional inheritance when discussing prototype, but as you’ll see, these examples don’t actually require use of the keyword. I’ve used my new favorite function, isPrototypeOf, to remedy that:

var A = {
value: 1,
};
var b = Object.create(A);
console.log(A.isPrototypeOf(b), b.value); // => true, 1
// aha! We have a prototype.
A.newValue = 2;
console.log(b.newValue); // => 2

Even though we don’t have to invoke the keyword to make this work, we have used prototypal inheritance to build the relationship between our prototype, A, and our new object, b. When we use Object.create, we are literally assigning one object as a prototype to another. Looking back at our definition, we see that prototype contains properties that can be referenced by other objects or functions. We’ve confirmed that A is a prototype of b, and we can also see that A’s value property can be referenced by b as its own.

Next, we see something that makes prototypal inheritance and delegation special. Because A is its own object, we can give it an additional property and see that b immediately reflects that change. Though our b object has already been created, it is constantly referencing A. If A changes, b changes.

As I was writing this post, I came to understand that functions will always have prototype as a property, but objects won’t. This was confusing because clearly objects were inheriting/referencing functions from their prototypes. For example:

var emptyObj = {};
emptyObj.prototype; // => undefined
emptyObj.toString(); // => “[object Object]”

Let’s look at our prototype definition one more time: it’s an object that contains properties for a constructed object or function. In JavaScript, functions can create objects using the constructor function:

var emptyFunc = function(){};
var newObj = new emptyFunc;

This is an ability unique to functions — objects can’t invoke constructor in the same way. They therefore have no use for prototype, but that certainly doesn’t mean that they can’t inherit properties. What we will see in all JavaScript objects and functions is a __proto__ property, containing all the properties that object has inherited. Here’s a cool trick:

var emptyObj = {};
emptyObj.__proto__ === Object.prototype; // => true

An object or function’s __proto__ property is a copy of the prototype of the function they inherit directly from. If an object inherits from multiple layers of prototypes, the __proto__ property will have a __proto__ of its own.

var myArray = [];
myArray.__proto__ === Array.prototype; // => true
myArray.__proto__ === Object.prototype; // => false
myArray.__proto__.__proto__ === Object.prototype; // => true
// wow!

I think that a big reason this has been so hard to process for me is that none of this code looks like what I’d normally write in JavaScript. It explains how things work under the hood, but we’re fortunate that a lot of this can stay under the hood while we’re writing our code. There’s definitely a use in examining a prototype to check if it’s going to pass down a certain property, as my company’s polyfill did in my last blog post, but it’s very very rare that we’ll ever edit a prototype. This technique, called “monkey patching,” can cause unwanted side-effects since we may be changing the very logic on which JavaScript runs. It might be coincidence, but after I ran this in my console, Google Docs literally stopped working. I had to refresh my window to fix things:

Sometimes learning about a new subject will return immediate dividends and give you ideas on how to write better code tomorrow. Though this wasn’t one of those times, better understanding the model on which JavaScript runs is invaluable and will certainly improve my work over time. I’ll again encourage those wanting to learn more to check out my sources. I’ve barely scratched the surface on what some of them are putting forward.

Solutions Engineer