Object Oriented Programming in Javascript
Programming Paradigms in Javascript
This is going to be a long topic that covers most of the general ways Object Oriented Programming (OOP) can be applied in the Javascript language. OOP is a programming paradigm that allows us to organise code in a way that is easy to understand and reason about. Another paradigm is Functional Programming(FP) which we will look at in later posts. Having well thought out and structured paradigms are important so that we can have code that is:
- Clear and understandable
- Easy to extend
- Easy to maintain
- Memory efficient
- Easily allows for other principles like DRY (Don't Repeat Yourself)
Today whenever working with a codebase, we're like to be working with one of these modern paradigms in contrast to early procedural programming like with assembly and other very low level languages, which did not allow for these - and thus made it much harder to work on large projects or for new developers to come in and maintain or extend programs.
In all programs there are two primary components. The data, and the behaviour of the program. In Javascript, the way the language is structured and organised was largely inspired by two other languages - Scheme and Java. The idea in Javascript that functions are first class citizens, and that we have closures is inspired from the Scheme languages implementation - which leads towards Functional Programming paradigm. The idea that we have Object inheritance is largely inspired by Javas implementation of Objects which leads towards the Object Oriented Programming paradigm. In Javascript, we're given the ability to work with both paradigms in a complementary manner, rather than viewing them as opponents to each other or mutually exclusive. By combining these techniques, as listed above we can make our code clear, easier to extend, maintain, and to be more memory efficient.
Object Oriented Programming
In OOP, the primary focus for organising our code is in Objects. Within objects, we will contain both our data, which can be referred to as state, as well as various methods. This model is fairly easy to logically understand as it is modelled very much on how we view real life. The properties of the object allow us to keep track of the objects state, and our methods allow us to manipulate the state of the object. In OOP there are two main types of languages. Class based languages (like Java), and prototype based languages. As established earlier, Javascript is a prototype based language.
To give an outline on how useful this is, we'll use an example to demonstrate the difference in code between Procedural based code and Object Oriented code. Let's suppose we wanted to make a basic fantasy game. To start, we'll have to create a few objects. Let's say we wanted to have elves in the game. A very naive approach could look like this:
const elf1 = {
name: "Rob",
weapon: 'bow',
attack() {
return 'attack with ' + elf.weapon
}
}
const elf2 = {
name: "Jenny",
weapon: 'bow',
attack() {
return 'attack with ' + elf.weapon
}
}
elf1.attack();
In this example, we have to repeat our code every time we want to add a new elf. Very quickly things would get out of hand and make for slow development. To make this more efficient, we're going to use a concept called factories.
//elf factory
function createElf(name, weapon) {
return {
// using ES6+ syntax we can make this even more concise, as if property and value are the same we don't need to specify the value
name: name,
weapon: weapon,
attack() {
return 'attack with ' + weapon
}
}
}
const elf1 = createElf('James', 'dagger')
elf1.attack()
const elf2 = createElf('Sam', 'bow')
elf2.attack()
This allows us to avoid repeating the same code each time we want to create an elf. This is great and much more efficient. However, what if we had thousands of elves? In this case, every new elf we create is taking up space in our memory for the same attack functions which are shared between all elves - despite the function being the same in each one. To make this more efficient, we can use prototypal inheritance as we covered in our previous post. Here's a naive approach:
// create a store for all shared functions for elves
const elfStore = {
attack() {
return 'attack with ' + this.weapon
}
}
//elf factory
function createElf(name, weapon) {
return {
// using ES6+ syntax we can make this even more concise, as if property and value are the same we don't need to specify the value
name,
weapon,
attack() {
return 'attack with ' + weapon
}
}
}
const elf1 = createElf('James', 'dagger')
elf1.attack = elfStore.attack
elf1.attack()
const elf2 = createElf('Sam', 'bow')
elf2.attack = elfStore.attack
elf2.attack()
As you can see here, we're going to have to again repeat ourselves linking each elf to the function store. To improve this, we can use Object.create to make our code more efficient.
// create a store for all shared functions for elves
const elfStore = {
attack() {
return 'attack with ' + this.weapon
}
}
// improved elf factory
function createElf(name, weapon) {
let newElf = Object.create(elfStore);
newElf.name = name;
newElf.weapon = weapon;
return newElf;
}
const elf1 = createElf('James', 'dagger')
elf1.attack()
const elf2 = createElf('Sam', 'bow')
elf2.attack()
Here we've made our code more efficient by using prototypal inheritance with Object.create() to make a link between our elf functions store and our elf objects. As we looked at with the prototypal inheritance post, this makes the elf proto object our elfStore, so we can access all the methods stored within there for our elves in one place in our memory. So even if we have thousands of elves, we aren't going to be using up any more memory for our shared functions. So now we have nicer, clean code and we're using prototypal inheritance in a way that it was intended to be used. But you won't find this type of code very often in Javascript projects, as this pattern is not used widely in the Javascript community. The reason is that this is still a little different from Object Oriented Programming.
Constructor Functions
To get code that is a little closer to what you'll see more commonly in Javascript projects, we'll take another approach. We will use Constructor functions instead which was used in older Javascript and is closer to traditional OOP.
// Constructor function
function Elf(name, weapon) {
this.name = name;
this.weapon = weapon;
}
// now have to add 'new' keyword to work with constructor function
const elf1 = new Elf('James', 'dagger')
const elf2 = new Elf('Sam', 'bow')
As we can see, we are now using the 'new' keyword to work with our constructor function. With this, we will return the object in our constructor function automatically. This 'new' keyword is always used with constructor functions in Javascript, as it also changes how the 'this' keyword operates by having it point to our object when 'this' is used - in our example, to elf1 or to elf2. If we were to not use the 'new' keyword, our function would not be creating an object, it would not be returning an object, and it would not be assigning the 'this' keyword to our declared elf objects. You might have also noticed now we're capitalising our 'Elf' function. This is because it is standard convention in Javascript to capitalise any constructor functions to help developers tell which functions are constructors at a glance.
So the new constructor function works by pointing the 'this' keyword to our newly created object (elf1). We then assign each of the properties and values we want on the new object with the 'this' keyword. Anything else defined within that function will not be assigned to the new object without the 'this' keyword. The constructor function we create is also given the 'prototype' property as it is a Function. Knowing this, we can then add any new methods we want onto this prototype to be shared by all of our objects that are created from the constructor function.
// Constructor function
function Elf(name, weapon) {
this.name = name;
this.weapon = weapon;
}
// add attack method to Elf function
Elf.prototype.attack = function() {
return 'attack with ' + this.weapon
}
// now have to add 'new' keyword to work with constructor function
const elf1 = new Elf('James', 'dagger')
const elf2 = new Elf('Sam', 'bow')
It is important to note that we cannot write our prototype functions here with modern arrow functions, as that would change the scope of our function from being dynamically scoped to being lexically scoped, so the 'this' keyword would not point to our object anymore.
So now that we've done this, have we finally approached OOP in Javascript similar to other languages? Not really! But this is one way we can use Javascript to code in a way similar to OOP in older versions of the language. The concept of prototype in Javascript is also tricky so if you don't understand the language deeply this code can definitely be very confusing. The key difference between this style of code and other OOP languages is that usually OOP is implemented using classes, which we haven't made use of here yet. It's still nice to look into older styles of programming in a language to deep dive into how it works and so we can be more familiar with older codebases if we ever come across them.
Classes in Javascript
With that all said, let's take a look at classes in Javascript. With ES6 Javascript finally got the 'class' keyword which allowed us to structure code in a more standard OOP way. This makes things a lot easier for developers to understand what is happening in the code compared to the older prototype style. Let's take a look at how this would look in our code:
//ES6 class
class Elf {
constructor(name, weapon) {
this.name = name;
this.weapon = weapon;
}
attack() {
return 'attack with ' + this.weapon
}
}
const elf1 = new Elf('James', 'dagger')
const elf2 = new Elf('Sam', 'bow')
With this style of organising our code, things are far more clear and understandable, as all of our code related to the Elf is contained within one area. Any time we need to change or add anything we can just update it in this area and all instances of the Elf (such as elf1 and elf2) class will be updated. The reason all of our data is contained within the constructor, whereas our methods are outside of it is that every time we create a new elf here, everything in the constructor is put into memory. We want our attack function to be shared across all elves for memory efficiency. If it was inside the constructor, we'd be creating a new copy of this every time we made a new elf. So how can we check if our elves properly inherit from a class? Similar to how we did 'isPrototypeOf' in our previous post about prototypal inheritance, here we can use 'instanceof'.
//ES6 class
class Elf {
constructor(name, weapon) {
this.name = name;
this.weapon = weapon;
}
attack() {
return 'attack with ' + this.weapon
}
}
const elf1 = new Elf('James', 'dagger')
console.log(elf1 instanceof Elf) // returns true as elf1 is an instance of the Elf class
Much like with the constructor functions, we also need to have the new keyword here whenever we are creating a new instance of a class. This is called instantiation. While this looks very similar to Object Oriented Programming you might see in other languages, under the hood it is still effectively the same as using the prototype example from before. The class, as with everything before is actually just an object, and it's still using the prototypal inheritance under the hood. While it's more common to see the more OOP oriented class style being used in modern codebases, both approaches work perfectly fine. It is simply a choice as to which one to use depending on the project, the team, and personal preferences.
Inheritance
Now that we're looking at classes and how to use those, we can look at another Inheritance, another one of the core principles of OOP. Inheritance is the passing down of data or knowledge in our classes. Suppose we wanted to expand our game example to include other characters. We could do a naive approach here and make a new goblin character by copying our elf. Let's see what happens if we do that and log the results.
class Elf {
constructor(name, weapon) {
this.name = name;
this.weapon = weapon;
}
attack() {
return 'attack with ' + this.weapon
}
}
const elf = new Elf('James', 'dagger')
const goblin = {...elf} // clone the elf as a goblin
console.log(goblin) // it seems like we have the same name and weapon as our elf as expected.
console.log(goblin.__proto__) // we get an empty object {} as our proto
console.log(elf.__proto__) // this gives us the Elf class
console.log(elf === goblin) // false - These objects are not referencing the same place in memory
goblin.attack() // even this won't work
So as we can see here, a simple cloning of properties will not make our new character inherit from the original class, so there is no access to the shared methods. It does not derive from the Elf class. Let's clean this up and make it a bit more in line with actual OOP structure by giving a base class of Character that all characters will derive from. This is called subclassing, in which our subclasses will inherit all of the original base class properties and methods, but can also extend it with new properties and functionality.
class Character {
constructor(name, weapon) {
this.name = name;
this.weapon = weapon;
}
attack() {
return 'attack with ' + this.weapon
}
}
// Make a new subclass for Elf that extends Character
class Elf extends Character {
constructor(name, weapon, type) {
// call Super keyword to run all of the Character base class constructor before we add anything, so that we can inherit its properties
super(name, weapon);
this.type = type;
}
}
class Goblin extends Character {
constructor(name, weapon, colour) {
super(name, weapon);
this.colour = colour;
}
// extend functionality by adding new methods
digCave() {
return this.name + ' digs a very deep cave'
}
}
const elf = new Elf('Jim', 'sword', 'Dark elf')
const goblin = new Goblin('Barry', 'stick', 'green')
goblin.digCave();
Here we create an elf that of the Elf class. Any time we now tell our elf to do anything, it will look up its inheritance chain to find whichever properties or methods we are calling, starting from the Elf class, and then going up to Character class. This allows us to share functionality between different types of characters when we want to add them. We always need to make sure to call 'super()' in our new constructor whenever we are extending a class. If we do not do this, none of the original properties will be defined.
We also can add a Goblin class that has extended functionality with new methods that all goblins will share, but other Characters or Elf classes will not share. This type of programming structure is extremely common in games development, but also in many other projects. It allows us to very easily share code and be very memory efficient as in Javascript we do not copy the properties and methods from higher classes, but instead point to them. It is also very readable and easy to understand and extend. As we mentioned earlier, under the hood this actually is the same as using 'Goblin.prototype.digCave = ', but in a much cleaner and easier to maintain way. This is one of the wonderful things about Javascript. If we continue on from our previous code we can see this in logs:
console.log(Goblin.prototype.isPrototypeOf(goblin)) // true
console.log(Character.prototype.isPrototypeOf(Goblin.prototype)) // true
// Because we're using classes, we can also see if our new characters are derived from classes with instanceOf
console.log(goblin instanceof Goblin) // true
console.log(goblin instanceof Character) // true, also inherits from Character
console.log(goblin instanceof Elf) // false, does not inherit from Elf
Of course, all of these examples are just small samples to show concepts. We can see this in many real life examples in Javascript with frameworks such as React.js (though newer versions use Functional Programming which we will cover in another post), and in many web projects.
Core Principles Of OOP
To recap, let's go over the four key pillars of Object Oriented Programming.
Pillar 1: Encapsulation
Encapsulation means organising all of our data objects and organises our things into units. We wrap our code into boxes that are related to one another so they can interact with each other using methods and properties we make available. This makes all of our code more reusable and easier to maintain. We have all these neat little class packages that we can just use as we need.
Pillar 2: Abstraction
Abstraction means hiding the complexity from our user. Creating simpler interfaces, such as taking care of the classes themselves and letting the user simply make an instance of the class and then they're ready to go. The idea of abstraction says: "Here are the methods and properties you can use. Don't worry about anything else, I'll do all the calculations behind the scene.". We can then just look at the methods and understand what the class can do.
Pillar 3: Inheritance
By inheriting from other classes we can avoid rewriting the same code, and we save memory space by having shared methods. This is a core part of using OOP.
Pillar 4: Polymorphism
Polymorphism means "many forms". In OOP, the idea is that we can call the same method on different objects and each object may respond in a different way. So we can use method overrides for example that will change how a method will act on a child class, or we can do method overloading which means adding different features to an already existing method (by calling the super of the original method within our new override). This allows us to reuse some of our functionality but also allowing us to customise methods to their own objects or classes. This is useful as we don't have to copy and paste code over and over.
This covers all of the core principles for OOP which combined give us code that is clear and understandable, easy to expand, easy to maintain, memory efficient, and helps with the DRY (Don't Repeat Yourself) principle.