Joshua Blewitt

WIBLTW 📖 - Object Oriented Programming in JavaScript!

Welcome to another 'what I've been learning this week' post. This week, it's about object oreiented programming (OOP) in JavaScript! Same as last time, check out the attached CodePen on this page to see a working example of what OOP is like!

With Objects, we can group data together with behaviours. In this case, the data is an object's attributes and the behaviour is functions (or methods). Objects can be very flexible, by being able to transfer data into the object by using a method in the object and even inheriting functionality from a parent object - this helps us to reduce code. Remember, Don't Repeat Yourself! (Or, DRY for short)

Let's jump in!

##A basic object Let's take a look at a basic object.

let Console = {
  name: "Machine",
  manufacturer: "Company",
  year: 1990,
  controllerPorts: 2
};

This creates a new object (defined by the use of curly brackets) called Console. It holds some information for us which is great and all but it doesn't do very much! Before we look at some other concepts, I want to show how we can access the properties in an object - by using dot notation!

console.log(Console.name);

This will print out 'Machine' to the console!

But this object is very limited. How can we make this better? We need to use constructors to allow us to make a instance of an object!

##Constructors and 'this'

We can use Constructors to make new objects, which is useful for making new instances of an object.

function Car(brand, model, automaticBool, fuelType, numWheels) {
  this.brand = brand;
  this.model = model;
  this.automaticBool = automaticBool;
  this.fuelType = fuelType;
  this.numWheels = numWheels;
}

This constructor also uses the keyword 'this' - which is a pretty big topic but in this case, it helps us reuse code. The properties defined in the object are called 'own' properties and they are defined on the instance of the object. With this in mind, we can now create new objects!

If you want to check if the constructor is equal to the constructor on an object, you can use this:

console.log(tesla.constructor === Car);

##Creating new objects

Creating a new object is done with the 'new' operator.

let tesla = new Car("Tesla", "Model 3", true, "Electric", 4);

When creating a new instance of an object, the properties need to have values so these are being passed in.

Now we can call the values held in the properties of the object by using dot notation!

console.log(tesla.brand);

##Prototype

Let's say we had a property called numDoors in a constructor (on the Car object) that had a hardcoded value of '4'. This means we will have duplicated the value inside each instance of Car. If there are a small amount of instances but if there are lots of instances - then this would be a lot of duplicated variables!

A better way to manage this (if the value in the property will be the same across all instances) is to use prototype.

Car.prototype.numDoors = 5;

Now this property is shared across ALL instances of Car! No more duplicates!

The problem with this approach is that after a few properties, this can become a bit tedious. A more efficient way is to set the prototype to a new object. This way, all the properties are added in one go.

Car.prototype = {
  constructor: Car,
  setPrice: function (price) {
    this.price = price;
  },
  setSeats: function (seats) {
    this.seats = seats;
  },
  setDoors: function (doors) {
    this.numDoors = doors;
  },
  printProperties: function () {
    console.log("Brand: " + this.brand);
    console.log("Model: " + this.model);
    console.log("Gear transmission: " + this.automaticBool);
    console.log("Fuel type: " + this.fuelType);
    console.log("Number of wheels: " + this.numWheels);
    console.log("Number of doors: " + this.numDoors);
    console.log("Number of seats: " + this.seats);
    console.log("Price: " + this.price);
  }
};

Now we can call these new functions on our existing object!

tesla.setPrice(40000);

tesla.setSeats(5);

tesla.setDoors(5);

tesla.printProperties();

You might notice at the top of the prototype, the constructor property is being set back to the name of the object - 'Car'. But why? This is because when changing the prototype, you erase the constructor property! So you will need to set the constructor property!

If you want to double check that if the object is an instance of the constructor, you can check it using the 'instanceOf' operator

console.log(tesla instanceof Car);

This will return either true or false (in this case it's true)

Fun fact about Prototypes - since (almost) every object in JavaScript has a Prototype, an objects Prototype is an object! This code will return 'Object':

console.log(typeof Car.prototype);

So that means a Prototype can have it's own prototype! This will return 'true':

console.log(Object.prototype.isPrototypeOf(Car.prototype));

##Own properties and Prototype properties

There are two types of properties; the Own properties that are defined on the object and Prototype properties that are defined on the prototype.

If you want to tell if a property is it's Own or a prototype, you can write a for statement which has an if statement to check if the property is it's Own property or not (and push the property into an appropriate array)

let ownProps = [];
let prototypeProps = [];

//for every property on the instance of an object, check if property is own or prototype, then push to appropriate array
for (let property in tesla) {
  if (tesla.hasOwnProperty(property)) {
    ownProps.push(property);
  } else {
    prototypeProps.push(property);
  }
}

console.log(ownProps);
console.log(prototypeProps);

##Inheritance

Inheritance is a useful way to ensure that you Don't Repeat Yourself (DRY!). If you repeat something in multiple places, that creates more work for you to do down the line if you need to fix something.

When it comes to OOP, we can share methods across various objects by creating a supertype (or parent object) like so:

//inheritance
//parent object
function Animal() {}
Animal.prototype.eat = function () {
  console.log("nom nom nom");
};
//Bird is a constructor that inherits from Animal
function Bird() {}
Bird.prototype = Object.create(Animal.prototype);
Bird.prototype.constructor = Bird;
//this adds behaviour that is unique to bird
//so this function can't be used in the animal object!
Bird.prototype.fly = function () {
  console.log("I'm flying!");
};

let parrot = new Bird();

parrot.eat();
parrot.fly();

Animal is the parent object in this case and has the method called 'eat'. When we create the constructor for Bird, we set the prototype to be of Animal. We do add a unique method to the Bird object called fly.

You can override inherited methods:

Bird.prototype.eat = function () {
  console.log("peck peck peck");
};

//this will print 'peck peck peck'
parrot.eat();

##Mixin functions

Mixin functions are handy as any object can use them!

let mixinFunction = function (obj) {
  obj.printMessage = function () {
    console.log("Hi, I'm a test!");
  };
};

mixinFunction(parrot);
parrot.printMessage();

##Protecting properties with closure

You can tell if a property is considered public if it can be accessed and modified outside of the objects definition, like so:

tesla.model = "Model Y";

This can make things easily changeable by other parts of the codebase, which could cause issues.

A way to prevent this is to make a private variable in the constructor function. The scope of the variable changes to be within the constructor only.

If you want to set or get the value in the variable, you need to use methods that are also within the constructor.

The example below shows this when the constructor called Computer has a private variable and two public methods:

function Computer() {
  //private variable
  let powerOnStatus = false;

  //public methods that instances of Computer can use
  //method to get the value held
  this.getPowerOnStatus = function () {
    return powerOnStatus;
  };
  //method to change the value held
  this.setPowerOnStatus = function (state) {
    powerOnStatus = state;
  }
}

let mac = new Computer();
// returns undefined
console.log(mac.powerOnStatus);
//returns false
console.log(mac.getPowerOnStatus());
//change to true
mac.setPowerOnStatus(true);
//return true
console.log(mac.getPowerOnStatus());

##Immediately Invoked Function Expression (IIFE)

These functions are executed as soon as they are declared!

(function () {
  console.log("Boom!");
})();

##Modules You can IIFE to group together functionality into a module.

let funModule = (function () {
  return {
    isCuteMixin: function (obj) {
      obj.isCute = function () {
        return true;
      };
    },
    singMixin: function (obj) {
      obj.sing = function () {
        console.log("Singing to an awesome tune");
      };
    }
  };
})(); //the two parentheses cause the function to be invoked immediately

funModule.singMixin(parrot);
parrot.sing();

You can then pass in an object to the function to allow the object to call that method!

##Conclusion

I hope you've enjoyed reading the second entry in WIBLTW!

My next post will be on Functional Programming.

Thanks for reading! 👋

Tags:


A photo of me!

I'm Joshua Blewitt, I'm passionate about product, a technology advocate, customer champion, curious mind and writer. I've worked for companies such as Rightmove, Domino's Pizza and IQVIA.

Let me know your thoughts!
More about me