TLDR: Yes. But as opposed to 'classical inheritance,' JS relies on prototypal inheritance.
Overview
This can be a very deep topic. The only reason that it's loosely relevant for this 'Pre-React' series is b/c you might want to start with class
-based components when beginning React.
Aside from that, since Object-Oriented Programming (OOP) is a deeply ingrained topic in software engineering in general, it may come up in some interviews. Some basic understanding of this topic, which is all that this article intends to provide, might be beneficial for that purpose also.
I'll focus on doing a couple of examples rather than doing too much theory. And... in the end, I'll kind of bash on OOP.
WTH is OOP?
Object-Oriented Programming. The gist of this concept is that one creates either a class (classical inheritance - C#/Java) or a prototype (prototypal inheritance - JS). This is commonly referred to as a blueprint to describe the features and characteristics of what something is supposed to be.
For example, a 'person blueprint' might require, hair color, eye color, height, weight, etc. to adequately describe a person. Along with this, we might encompass functionality associated with a person - eating, sleeping, etc.
So, with that, we have each and everything to 'model' a person. From there, we can pass use that 'blueprint' to model more specific people.
For example, a 'driver' is a 'person' but may include 'driving' functionality.
OOP focuses on inheritance. This means that we have to classify/categorize things in terms of is a relationship. A 'driver' is a 'person.' A 'student driver' is a 'driver', which is also a 'person.'
Summarily, the purpose of OOP is to dynamically generate instances or objects of a specific type with 'built in' properties and methods w/o having to start from scratch each and every time.
Creating Instances 'On The Fly'
To consider why we might even care about OOP at all, let's just create some individual objects - i.e. instances - of a couple of people. We'll do 'students' and 'faculty.'
If you've kept up with this series, pretty much all of the code should seem pretty familiar.
this
, in case you're wondering π€ is just making sure that whenever one of the methods is called, it will be properly bound to the correct object literal, and that it will use the correct properties. Without this
JS will error out as it will look for, for example, name
on the global object π
π½ββοΈ.
Moving on, the π observation that we make ππ½ is the code duplication π π½ββοΈ. We need to DRY (Don't Repeat Yourself) it up. This is why we might use OOP, taking advantage of JS's prototypal inheritance feature.
After all, this would be very tedious and waste a lot of memory π§ if we have to make 5000 students, etc.
Function Constructors
Although it's rare to see this in JS nowadays except in legacy code, it behooves us to grasp the nuances of prototypal inheritance.
Person
Instance Properties
We'll create a constructor function that will encapsulate the properties that all people should have.
function Person({id, name, age} = {}) {
this.id = id;
this.name = name;
this.age = age;
}
function Person
- It's a convention to capitalize function constructors. This signifies that we should use the new
keyword to create individual instances using this particular function constructor.
({id, name, age} = {})
- We are expecting to receive a single 'configuration' object literal, from which we will destructure the πs. = {}
is just nice to have so that if we accidentally invoke the function without any arguments, at least our program won't just crash out. It's meant to simulate named parameters from other languages.
this
- When used in conjunction with the 'new' keyword ππ½,
this
will properly instantiate the instance, ensuring that the 'individual properties' are properly bound to the 'new instance.'
Shared Functionality
All Person
s should be able to greet()
.
/ β οΈ 'Fat arrow' syntax will NOT properly bind 'this' β
Person.prototype.greet = function greet() {
return `ππ½. My name is, ${this.name}.`;
};
prototype
- this is the crux of our JS OOP's prototypal inheritance model. It establishes a chain βοΈ such that whenever we call a method on an instance, unless that instance has its own 'special implementation' (more on this later), this 'shared prototype method' will be used instead.
Again, this
ensures that we reference the name
of the instance that is currently using this method.
Extend Person
to Student
and Faculty
The crux of the inheritance - establishing that Student
s and Faculty
s are Person
s occurs in the following areas:
// Inheritance
Person.call(this, { id, name, age });
ππ½ Here, we are invoking call
(this is also another deep topic, along with bind
, so we'll stick to the basics) directly on the 'parent' constructor function. Once again, this
comes into play b/c we need to let Person
know that this
is supposed to 'bind to' a Student
(or Faculty
) when it's called.
The second argument uses object shorthand to create an object literal argument that Person
uses to do its part for Student
or Faculty
Student
allows Person
to instantiate some of its properties, and it focuses on just the ones that are 'special' to it.
/**
* Inheritance -
* Spread the 'Person' prototype as a separate reference in
* the 'Student.prototype'.
*
* This means that 'Student' inherits from 'Person'
* But, if we add more functionality to 'Student',
* it will not affect 'Person.'
*/
Student.prototype = {...Person.prototype};
We also allow Student
to inherit any/all functionality encapsulated by Person
(greet
), in this case. Likewise, we see: Faculty.prototype = Person.prototype;
Customize rudeKid
rudeKid.greet = function() {
return `I'm ${this.name}. Get bent! ππ½`
}
Although rudeKid
is a Student
, which inherits from a person, rather than traversing the prototype chain βοΈ, JS sees that rudeKid
has its own greet
method and uses that one. This is pretty π. We can easily customize 'special functionality' for any given instance, while also inheriting.
prototype
If we search around in MDN documentation, we see many mentions of prototype
. For example, Array.prototype.map()
.
This means that whenever we create an instance of an array, and, for example, invoke map
, we are using some 'shared functionality' among all arrays via prototype.
This makes sense. We don't want to waste memory π§ by having all of our 'array methods' duplicated for each and every array π
π½ββοΈ!
Even though we can ππ½, you should not ever overwrite the 'built-in' JS stuff. The example above does give some clue as to how some folks can create 'special JS libraries' that can 'expand upon' its built-in functionality. These would need to properly namespace though so that it expands upon and doesn't replace built-in JS functionality.
class
π¦ That's a lot of work ππ½. Fortunately, as part of ES2015/ES6, JS 'borrowed' some syntax from 'classical OOP' languages such as C#/Java.
class
is just syntactic sugar π§. Nothing, and I mean nothing about JS' prototypal inheritance as illustrated with function constructors ππ½ changes.
I will emphasize it again. If you are coming from the class
keyword in other programming languages, note π΅ that the behavior of class
is significantly different. This is why I took time to show the 'old way' ππ½; hopefully it is clear that we are borrowing 'classical inheritance syntax,' but the 'under the hood' implementation is still prototype-based.
Essentially, all the things work the same way, but the syntax is a bit more delightful π€.
super
takes the place of using call(this
.
extends
takes the place of .prototype = Person
assignment.
Anti-OOP π§
β οΈ It's arguable that this is generally a poor design approach as I have to 'upfront' try to predict all of the different 'things' that I may need in an application, and could be severely restricted with constantly classifying what things are (as opposed to what they 'do,' a la composition).
I'll show composition in the next post.
Top comments (3)
Since ES6, Javascript is the exact same thing as C#, C++, and Java with respect to 'classical inheritance'.
The 'class' and 'extends' keywords took care of that.
How it's done at run time is just adminis-trivia.
Not only that, use of explicit properties, using the Class construct is just a lot cleaner. Less typing and easy to read.
Over-rideable properties exist as well, merely by using getters and settlers.
The biggest issue I've seen in the Javascript world is
This article reflects that attitude in my opinion.
Javascript today fully supports 'classical inheritance' through the 'class' but always had with the prototype prior to EC6.
The prototypal inheritance does offer some potential advantages and does differ from 'classical inheritance.
For example, in classical OOP, we have to deal with interfaces and other complexity just do something like is shown where
rude
can easily implement the functionality 'on the fly' to a specific instance - in line of code.I welcome you to write a formal article showing similar examples with 'purely classical' inheritance, as I don't profess to be any type of expert in 'classical OOP.'
In one of my next posts, I will focus on not necessarily needing to do anything OOP and use composition instead.
This old, but gr8 video kind of sets the table for that: .
Sound confusing? Why would we do this on just a person model class? Answer: We wouldn't, because the Person class with just 2 properties is sufficient and the interface is implicit.
There's two types of inheritance 'classical' which is vertical nature, it deals with classes, and subclasses. The interface (explicit or implicit) is 'horizontal in nature' and is seen as the ability to pass something in to be contained, it can be done via parameters or by object placeholders with the class or function. The concepts of composition use the horizontal style while classical inheritance is the vertical style.
It all boils down to one simple concept, can I morph my object in such a way that I can only see the properties or functions I want? If the answer is yes then we are using some form on inheritance/composition.
BTW, I saw that video years ago and disagreed with the general conclusions then.