JavaScript Pseudoclassical Subclasses
When we talk about classes and subclasses, what are we talking about usually? Inheritance! That is, the ability of one object to have access to the properties and methods of another object.
Before reading up on subclasses, it is helpful to understand how classes/constructors work. Essentially, they allow you to build multiple objects with similar properties and methods. If you’re coming from a different language, it’s important to know that, in JavaScript, they are a bit unique (even with the new class keyword in ES6).
A previous HackReactor student wrote a really great blog introducing the different implementations of functional classes (different instantiation patterns). It even includes a graphic with side by side comparisons of each. I suggest that before reading on about subclasses you take a look at his post.
Keep in mind that with each of the four different instatiation patterns - functional, functional-shared, prototypal, and pseudoclassical - the goal remains the same, but the method used to get there is what differs. From what I have gathered, there is no wrong or right way, but there are strong preferences and some, like prototypal and pseudoclassical, make better use of the unique characteristics of JavaScript.
Below, we’ll be using the pseudoclassical instatiation pattern in discussing subclasses.
Let’s geek out.
Right now, we have a constructor, ‘Being()’, with some properties and methods.
We could use it to create a bunch of little walking, talking beings:
Pretty boring. Well, what if we use ‘Being()’ as the parent class to build out a few of the different Lord of the Rings characters? Hobbits also move, eat, and speak, but they also have some pretty unique characteristics. How could we have it so that our Hobbits have the same properties and access to the methods of ‘Being()’ plus more? We use subclassing.
Let’s explore the above code a bit more to understand what is happening.
On line 1 we are declaring our ‘Hobbit()’ constructor (remember that it is common practice to capitalize the first letter of your constructor). We are also passing in two arguments - ‘name’ and ‘characteristic’.
On line 2 it looks like something kinda funky’s happening. Remember we said that we wanted ‘Hobbit()’ to have some of the same properties as ‘Being()’. So we could have gone ahead and rewritten those properties within ‘Hobbit()’ ourselves. But that’s not efficient, especially if we had 50 different properties to rewrite. The .call method allows us to invoke ‘Being()’ while binding the keyword ‘this’ to the object returned by ‘Hobbit()’.
Remember that when we instantiate an object using the ‘new’ keyword, like we would with ‘Hobbit()’, in the background ‘this’ is assigned to a new empty object that later gets returned. That is precisely what we are passing through when we write ‘Being.call(this, name, characteristics)’.
After calling line 2, our ‘this’ within ‘Hobbit()’ that was previously an empty object now includes the properties ‘this.name = name’, ‘this.legs = 2’, ‘this.arms = 2’, ‘this.body = true’, ‘this.charachteristic = charachteristic’.
Lines 3 and 4 are pretty simple - we are adding a ‘height’ and ‘feet’ properties to our object. These two properties, ‘height’ and ‘feet,’ will be unique to objects instantiated using the ‘Hobbit()’ constructor.
Line 5 is a bit more interesting:
Constructors come with a special ‘.prototype’ property that stores methods each new instance will have reference to via the prototype chain. Without line 5, were we to try to call bilbo.move(), we would get an error.
On line 5, ‘Object.create(Being.prototype)’ creates a new object whose own prototype is ‘Being.prototype’. That means that every instance of ‘Hobbit()’ will now be able to delegate it’s methods first to it’s own prototype, and if it doesn’t find it there, it can look to see if the method exists in ‘Being.prototype’. Now when we call ‘bilbo.move()’ it will first look to see if bilbo itself has a ‘.move()’ method, and since it does not, it looks down the prototype chain and sees ‘.move()’ on ‘Being’ - ‘bilbo’ is now able to move!
Remember I mentioned that all constructors come with a special ‘.prototype’ property? Well, that prototype property itself contains a ‘.constructor’ property that stores a reference to the constructor that was used to instantiate that object. After line 5, ‘Hobbit’’s ‘.constructor’ property was overwritten. It looks down the prototype chain and finds ‘Being()’’s ‘.constructor’ property:
The above would suggest that ‘bilbo’ was instantiated using ‘Being’, but that’s not accurate, we used ‘Hobbit()’. There’s an easy fix, add the appropriate ‘.constructor’ property back in:
Now when we log ‘biblo.constructor’ we will be able to see that ‘bilbo’ was instantiated using the ‘Hobbit’ constructor.
Lastly, on lines 7 and 8 we are adding methods to the ‘Hobbit.prototype’ that will only be accessible to instances of ‘Hobbit’ (like ‘bilbo’).
We’re almost done! What if we want instances of ‘Hobbit()’ to speak but we want them to say more than just ‘Hello’ like it says on ‘Being.prototype.speak()’? We can actually mask the ‘.speak()’ method on ‘Being()’ by writing our own for ‘Hobbit()’:
We could go on and on creating more subclasses (like types of Hobbits!):
Summary
That was a whole lot - let’s do a quick summary:
- Superclasses, like ‘Being()’, hold default properties and methods that will be able to be shared with subclasses, like ‘Hobbit()’.
- Subclasses are able to inherit properties and methods from their superclass, while also creating properties and methods unique to only them.
- Subclasses are unable to change the values of properties on the superclasses, but are able to mask them by writing a property of their own with the same name.
- Instances of both supercalsses and subclasses are able to delegate method lookup through the prototype chain.
- When writing ‘Child.prototype = Object.create(Parent.prototype)’ we are pointing ‘Child()’’s .prototype to the ‘Parent()’ .prototype.
- So what? It means we end up writing a whole lot less code than we otherwise would have, and using the prototype chain, each instatiation can delegate its method lookups instead of replicating methods.
Below is the code all together: