Hopp til hovedinnhold

Have you ever inspected an object in your browser's console and stumbled upon the field [[Prototype]] or __proto__. Or maybe you have seen someone use the mystical prototype field on classes, such as String.prototype. Ever wondered what kind of sorcery this prototype is? Great! ️This article will hopefully make things a bit clearer, and make you an advanced prototype wizard. 🧙‍♂️

Categories of Object-Orientation

For us to understand what prototypes are we are going to have to talk about categories of object-orientation. Hold on! Before you close this article, I promise this isn't going to be one of those articles that go into the nitty-gritty of programming language theory. I intend only to poke at the surface of the rabbit hole, so this should be digestible by anyone who has basic knowledge of object-oriented programming.

Still here? Awesome! Let's briefly look at two different categories of object-orientation, namely class-based and prototype-based object-orientation. 🤓

Class-based vs. Prototype-based Object-Orientation

Class-based and prototype-based object-orientation are categories referring to how inheritance is handled in the language. In class-based languages, such as Java, inheritance is defined in the class definitions through super-/sub-class relations, while in prototypical languages, objects inherit properties directly from other objects.

What does that even mean? Well, instead of JavaScript having classes that contain the objects' shared properties, such as methods, JavaScript instead uses other objects to store this. These objects are what we call prototypes. So all instances of the same "class" in JavaScript will share the same prototype object.

Why does JavaScript use Prototypes when it also has Classes?

Here's the cool part, there aren't really any classes in JavaScript! 🤯

The class-syntax in JavaScript is just syntactic sugar for creating a prototype and an object-constructor function. Confused? Let's look at what this means.

The following example shows how you could implement a Santa class in JavaScript. The Santa class has some property hasEatenCookies which is set in the constructor of the class. In addition, we also have a method to check if Santa is hungry. 🎅🍪🤰

class Santa {
   constructor(hasEatenCookies) {
      this.hasEatenCookies = hasEatenCookies;
   }
  
  isHungry() {
    return this.hasEatenCookies === false;
  }
}

Since JavaScript doesn't really have classes, this would be transformed into a prototype object and a function for constructing Santa objects, like the following:

const santaPrototype = {
  isHungry: function() {
    return this.hasEatenCookies === false;
  }
}

function santaConstructor(hasEatenCookies) {
  const santaObject = {
    hasEatenCookies: hasEatenCookies
  }
  Object.setPrototypeOf(santaObject, santaPrototype)
  return santaObject
}  

Note: This is a somewhat simplified example of a class transformation, if you would like to see a more complete transformation you can check out this Babel REPL.

As we can see, the methods of the Santa class has been put into a normal object, while the constructing of objects has been made into a standard function. The santaConstructor function simply creates an object with the hasEatenCookie property, and the object is then assigned the santaPrototype.

How does Inheritance work with Prototypes?

It is actually quite simple. Since prototypes themselves are just objects, they can themselves also have prototypes. So if we made a subclass, BadSanta, of our Santa class, this would result in our BadSanta prototype having the Santa prototype as its prototype...

That was a lot of prototype at once. Let's instead look at some code. Below we have implemented our BadSanta class, which introduces a method to check if BadSanta cares if you've been naughty or nice.

class BadSanta extends Santa {
  constructor(hasEatenCookies) {
    super(hasEatenCookies)
  }
  
  function caresIfYouveBeenNaughtyOrNice() {
    return false;
  }
}

We could then create an instance of BadSanta and call the methods from both our BadSanta and our Santa class.

const willie = new BadSanta(false);
willie.caresIfYouveBeenNaughtyOrNice(); // false
willie.isHungry(); // true

What is happening here is that for every time we try to reference an attribute on an object, there's a check performed to see if the attribute exists on the object. If it doesn't exist it will do the same check on its prototype, recursively.

So when we do willie.isHungry(), JavaScript will check if isHungry exists on the object willie. Since willie doesn't have any attribute with that name it will check its prototype, BadSanta. BadSanta doesn't have this attribute either, so it will check its prototype, Santa. Fortunately, the Santa prototype has this attribute, and we can use this function for our function call.

That's it!

As we have seen, prototypes aren't all that mystical. They are just objects used to share properties between objects, such as the methods we would define in a class. We also saw how inheritance is handled in JavaScript, where class-based languages has classes with superclasses, JavaScript instead has prototypes with parent prototypes. This is possible since prototypes themselves are just objects.

Wait, it's all objects? Always has been.

Relevant resources recommended by the author

Did you like the post?

Feel free to share it with friends and colleagues