DEV Community

loading...
Cover image for Is JS an OOP Language?

Is JS an OOP Language?

codefinity profile image Manav Misra Updated on ・5 min read

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;
}
Enter fullscreen mode Exit fullscreen mode

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 Persons should be able to greet().

/ ⚠️ 'Fat arrow' syntax will NOT properly bind 'this' 
Person.prototype.greet = function greet() {
  return `👋🏽. My name is, ${this.name}.`;
};
Enter fullscreen mode Exit fullscreen mode

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 Students and Facultys are Persons occurs in the following areas:

// Inheritance
Person.call(this, { id, name, age });
Enter fullscreen mode Exit fullscreen mode

👆🏽 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};
Enter fullscreen mode Exit fullscreen mode

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! 🖕🏽`
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (4)

pic
Editor guide
Collapse
jwp profile image
John Peters

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

wholesale rejection of OOP, without knowing how to use it properly.

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.

Collapse
codefinity profile image
Manav Misra Author

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: .

Collapse
jwp profile image
John Peters

For example, in classical OOP, we have to deal with interfaces
We only have to deal with interfaces when they are implemented as an entry to a function or method.
Consider this in C#

// Classes have implicit interface definitions
// plus the ability to create a new object of that type
public class Person
{
    // implicit interface properties of...
    string lastName;
    string firstName;
}
// if there's a method  anywhere in the project like this:

setName(Person person){
  // the parameter is a concrete Person object
  // that has implicit interfaces of lastName and firstName properties.
}

// but what about this?
setName(iPerson person){}

// and assume this:
// a definition of an interface
interface IPerson{
  string lastName;
  string firstName;
}
// here we have the same exact thing happening, 
// but the parameter is asking for an object of type interface
// and not a concrete person object.

// The only difference is that we cannot create a new instance of an interface. 
// To do that we'd need this.
// Change Person to implement the interface
class Person:IPerson{
  string lastName;
  string firstName;
}

//create a new person object which passes the interface test
var person = new Person()
// Works due to our Person Class
// implementation of iPerson
setName(iPerson person){}  

Enter fullscreen mode Exit fullscreen mode

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.

Collapse
zoedreams profile image
☮️✝️☪️🕉☸️✡️☯️

check out protocol design pattern.