JavaScript OOP

Object-Oriented Programming, OOP, refers to using self-contained pieces of code to develop applications. We call these self-contained pieces of code objects. We use objects as building blocks for our applications. Building applications with objects allows us to adopt some valuable techniques such as;

  • Inheritance, objects can inherit features from other objects,
  • Polymorphism, objects can share the same interface—how they are accessed and used—while their underlying implementation of the interface may differ, and
  • Encapsulation, each object is responsible for specific tasks.

In this article, we are concerned with only Inheritance and Encapsulation since only these two concepts apply to OOP in JavaScript, particularly because in JavaScript, objects can encapsulate functionality and inherit methods and properties from other objects.

This article does not cover ES6 new class keyword for OOP in JavaScript, that is another article coming soon.

Encapsulation and Inheritance in JavaScript

Objects can be thought of as the main actors in an application, or simply the main “things” or building blocks that do all the work. As you know by now, objects are everywhere in JavaScript since every component in JavaScript is an Object, including Functions, Strings, and Numbers. We normally use object literals, or the constructor functions to create objects.

Encapsulation refers to enclosing all the functionality of an object within that object so that the object’s internal workings, its methods and properties, are hidden from the rest of the application. This allows us to abstract or localize specific set of functionality on objects.

Inheritance refers to an object being able to inherit methods and properties from a parent object, a Class in other OOP languages, or a Function in JavaScript.

Both of these concepts, encapsulation and inheritance, are important because they allow us to build applications with reusable code, scalable architecture, and abstracted functionality. Maintainable, scalable, efficient.

An instance is an implementation of a Function. In simple terms, it is a copy, or “child” of a Function or object. For example:

// Tree is a constructor function because 
// we will use new keyword to invoke it.
function Tree(typeOfTree) {} 

// appleTree is an instance of Tree.
const appleTree = new Tree("apple")

In the preceding example, appleTree is an object that was created from the Tree constructor function. We say that the appleTree object is an instance of the Tree object. Tree is both an object and a function, because functions are objects in JavaScript. appleTree can have its own methods and properties and inherit methods and properties from the Tree object, as we will discuss in detail when we study inheritance below.

OOP in JavaScript

The two important principles with OOP in JavaScript are Object Creation patterns (Encapsulation), and Code Reuse patterns (Inheritance). When building applications, you create many objects, and there exist many ways for creating these objects: you can use the ubiquitous object literal pattern, for example:

const myObj = {name: "Henrik", profession: "Software Developer"}

You can use the prototype pattern, adding each method and property directly on the object’s prototype. For example:


function Consultant() {}

Consultant.prototype.firstName = "Henrik"
Consultant.prototype.lastName = "Grönvall"
Consultant.prototype.startDate = new Date(Today())
Consultant.prototype.fullName = function () {
  console.log (this.firstName + " " + this.lastName); 
}

let henrikGr = new Consultant() 
console.log(henrikGr.fullName()) // Henrik Grönvall

You can also use the constructor pattern, a constructor function, Classes in other languages, but Functions in JavaScript. For example:

// Consultant() is the constructor function because 
// we use the new keyword below to invoke it.
function Consultant(name, profession) {
  this.name = name
  this.profession = profession
} 

// henrikGr is a new object we create from 
// the Consultant() constructor function.
let henrikGr = new Consultant(“Henrik”, “Software Developer”) 

console.log(henrikGr.name)  //Henrik
console.log(henrikGr.profession)  // Software Developer

In the latter example, we use a custom constructor function to create an object. This is how we create objects when we want to add methods and properties on our objects, and when we want to encapsulate functionality on our objects. JavaScript developer have invented many patterns for creating objects with constructor functions. And when we say Object Creation Patterns, we are concerned principally with the many ways of creating objects from constructor functions, as in the preceding example.

In addition to the patterns for creating objects, you want to reuse code efficiently. When you create your objects, you will likely want some of them to inherit (have similar functionality) methods and properties from a parent object, yet they should also have their own methods and properties. Code reuse patterns facilitate ways in which we can implement inheritance.

These two universal principles — creating objects (especially from constructor Functions) and allowing objects to inherit properties and methods — are the main focus of this article and, indeed, the main concepts with OOP in JavaScript.

Encapsulation in JavaScript

To implement encapsulation in JavaScript, we have to define the core methods and properties on that object. Before we implement it, let’s quickly learn a bit more about the practicality of encapsulation.

Why Encapsulation?

When you simply want to create an object just to store some data, and it is the only object of its kind, you can use an object literal and create your object. This is quite common and, you will use this simple pattern often.

However, whenever you want to create objects with similar functionality, to use the same methods and properties, you encapsulate the main functionality in a Function and, you use that Function’s constructor to create the objects. This is the essence of encapsulation, and it is this need for encapsulation that we are concerned with.

To make practical use of OOP in JavaScript, we will build an object-oriented quiz application that uses all the principles and techniques we learn in this article. First up, our quiz application will have users (a Users Function) who take the quiz. There will be some common properties for every user who takes the quiz: each user will have a name, a score, an email, and the quiz scores (all the scores). These are the properties of the User object. In addition, each User object should be able to show the name and score, save scores, and change the email. These are the methods of the object.

Because we want ALL the user objects to have these same properties and methods, we cannot use the object literal way of creating objects. We have to use a constructor Function to encapsulate these properties and methods.

Since we know all users will have the same set of properties, it makes sense to create a Function, Class in OOP languages, that encapsulates these properties and methods.

Implementation example

A User Function example:

function User(name, email) {
  this.name = name
  this.email = email
  this.quizScores = []
  this.currentScore = 0
}

User.prototype = {
  constructor: User,
  saveScore: function(score)  {
    this.quizScores.push(score)
  },
  showNameAndScores: function()  {
    let scores = this.quizScores.length > 0 ? this.quizScores.join(",") : "No Scores Yet"
    return this.name + " Scores: " + scores
  },
  changeEmail:function(email)  {
    this.email = email
    return "New Email Saved: " + this.email
  }
}

Then we can create an instance of the User object:

// A User 
firstUser = new User("Henrik", "henrik@examnple.com") 
firstUser.changeEmail("henrik.gronvall@examnple.com")
firstUser.saveScore(15)
firstUser.saveScore(10)

firstUser.showNameAndScores() //Henrik Scores: 15,10

// Another User
secondUser = new User("Lars", "lars@examnple.com")
secondUser.saveScore(18)
secondUser.showNameAndScores()  //Lars Scores: 18

Let’s expound on each line of the code in the User function, so we have a thorough understanding of this pattern.

The lines 2-5 initialize the instance properties. These properties will be defined on each User instance that is created. So the values will be different for each user. The use of this keyword inside the function specifies that these properties will be unique to every instance of the User object:

In the lines 9-20, we are overwriting the prototype property with an object literal, and we define all of our methods, that will be inherited by all the User instances, in this object.

This way of overwriting the constructor is simply for convenience, so we don’t have to write User.prototype each time, like this:

User.prototype.constructor = User
User.prototype.saveScore = function(score)  {
    this.quizScores.push(score)
}

User.prototype.showNameAndScores = function()  {
    let scores = this.quizScores.length > 0 ? this.quizScores.join(",") : "No Scores Yet"
    return this.name + " Scores: " + scores
}

User.prototype.changeEmail = function(email)  {
    this.email = email
    return "New Email Saved: " + this.email
}

By overwriting the prototype with a new object literal we have all the methods organized in one place, and you can better see the encapsulation that we are after and, of course it is less code you have to type.

JavaScript Prototype In JavaScript, you add methods and properties on the prototype property when you want instances of an object to inherit those methods and properties. This is the reason we add the methods on the User.prototype property, so that they can be used by all instances of the User object. You can read more in my previous article about prototype

Constructor Property Every function has a constructor property, and this property points to the constructor of the function. For example:

function Fruit() {}
const newFruit = new Fruit()
console.log(newFruit.constructor) // Fruit ()

The one disadvantage of overwriting the prototype is that the constructor property no longer points to the prototype, so we have to set it manually:

constructor: User

Prototype Methods In the following lines, we create methods on the prototype so that all instances of Users can have access to these methods.

saveScore: function(score)  {
  this.quizScores.push(score)
},
showNameAndScores: function()  {
  var scores = this.quizScores.length > 0 ? this.quizScores.join(",") : "No Scores Yet"
  return this.name + " Scores: " + scores
},
changeEmail: function(email)  {
  this.email = email;
  return "New Email Saved: " + this.email;
}

Then we created instances of the User object:

// A User 
firstUser = new User("Henrik", "henrik@examnple.com") 
firstUser.changeEmail("henrik.gronvall@examnple.com")
firstUser.saveScore(15)
firstUser.saveScore(10) 

firstUser.showNameAndScores()  // Henrik Scores: 15,10

// Another User
secondUser = new User("Lars", "lars@examnple.com")
secondUser.saveScore(18)
secondUser.showNameAndScores()  //Lars Scores: 18

As you see, we have encapsulated all the functionality for a User inside the User Function, so that each instance of User can make use of the prototype methods (like changeEmail) and define their own instance properties, like name and email.

With this pattern, you can use the standard operators and methods on the instances, including the instanceOf operator, the for-in loop, even hasOwnProperty, and the constructor property.

Inheritance in JavaScript

Implementing inheritance in our quiz application will permit us to inherit functionality from parent Functions to reuse code in our application and extend the functionality of objects. Objects can make use of their inherited functionality and still have their own specialized functionality.

Let’s see why its practical to use inheritance in our example application.

We have successfully implemented encapsulation by enclosing all the functionality for users of our quiz application by adding all the methods and properties that each user will need on the User function, and all instances of User will have those properties and methods.

Why Inheritance?

Next, we want to encapsulate all the functionality for every Question. The Question function, Class in OOP languages, will have all the generic properties and methods that every kind of question will need to have. For example, every question will have the question, the choices, and the correct answer. These will be the properties. In addition, each question will have some methods: getCorrectAnswer and getUserAnswer, and displayQuestion.

We want our quiz application to make different types of Questions. We will implement a MultipleChoiceQuestion function and, a DragDropQuestion function. To implement these, it would not make sense to put the properties and methods outlined above - that all questions will use - inside the MultipleChoiceQuestion and DragDropQuestion functions separately, repeating the same code. This would be redundant.

Instead, we will leave those properties and methods - that all questions will use - inside the Question object and make the MultipleChoiceQuestion and DragDropQuestion functions inherit those methods and properties. This is where inheritance is important: we can reuse code throughout our application effectively and better maintain our code.

Since the MultipleChoiceQuestion HTML layout will be different from the DragDropQuestion HTML layout, the displayQuestion method will be implemented differently in each. So we will override the displayQuestion method on the DragDropQuestion. Overriding functions is another principle of OOP.

Implementing Inheritance

To implement inheritance we have to use two techniques specifically for inheritance in JavaScript.

  • creating children objects that inherits from a parent
  • overriding methods

Creating children

Creating our quiz OOP style, lets go ahead and implement our Question constructor first. This can be thought of as a super class for Questions.

/**
 * Question constructor
 * All question objects will inherit from this
 * @param theQuestion
 * @param theChoices
 * @param theCorrectAnswer
 * @constructor
 */
function Question(theQuestion, theChoices, theCorrectAnswer) {
  // Initialize the instance properties
  this.question = theQuestion;
  this.choices = theChoices;
  this.correctAnswer = theCorrectAnswer;
  this.userAnswer = "";

  // private properties: these cannot be changed by instances
  const newDate = new Date()
  // Constant variable: available to all instances through the 
  // instance method below. This is also a private property.
  const QUIZ_CREATED_DATE = newDate.toLocaleDateString()

  // This is the only way to access the private QUIZ_CREATED_DATE 
  // variable 
  // This is an example of a privilege method: it can access 
  // private properties and it can be called publicly
  this.getQuizDate = function() {
    return QUIZ_CREATED_DATE;
  }

  // A confirmation message that the question was created
  console.log("Quiz Created On: " + this.getQuizDate())
}

Next, we are adding prototype methods to the Question objects, all instances of the Question object will inherit these methods, because we are adding the methods on the Question prototype.

// Define the prototype methods that will be inherited
Question.prototype.getCorrectAnswer = function() {
  return  this.correctAnswer
}

Question.prototype.getUserAnswer = function() {
  return this.userAnswer
}

Question.prototype.displayQuestion = function() {
    let questionToDisplay = this.question
    let choiceCounter = 0

    this.choices.forEach(function (eachChoice)  {
        questionToDisplay += '<span>' + eachChoice + '</span>'
        choiceCounter++
    });

    console.log(questionToDisplay)
}; 

Now that we have the Question constructor object setup, we can inherit from it and create sub classes (children objects). The power of inheritance is that we can create all sorts of questions now, and each can be quite versatile.

Now we are creating the Multiple Choice Question:

/**
 * MultipleChoiceQuestion constructor
 * For MultipleChoiceQuestion to properly inherit from Question,
 * here inside the MultipleChoiceQuestion constructor, we have to
 * explicitly call the Question constructor
 * passing MultipleChoiceQuestion as the this object, and the parameters
 * we want to use in the Question constructor:
 * @param theQuestion
 * @param theChoices
 * @param theCorrectAnswer
 * @constructor
 */
function MultipleChoiceQuestion(theQuestion, theChoices, theCorrectAnswer) {
  // Call the Question constructor
  Question.call(this, theQuestion, theChoices, theCorrectAnswer);
}

Then we have to inherit the prototype object from Question:

// inherit the methods and properties from Question
MultipleChoiceQuestion.prototype = Object.create(Question.prototype)

Now, we have ensured that the child, MultipleChoiceQuestion inherits methods from the Question object and when creating a new MultipleQuestion object it will also call the parent constructor first.

We have one thing left to do before everything is ok, we need to reassign the MultipleChoiceQuestion constructor because when we inherited the parent's prototype, we lost the constructor:

MultipleChoiceQuestion.prototype.constructor = MultipleChoiceQuestion;

Now we can make yet another type of question, the drag and drop question:

/**
 * DragDropQuestion constructor
 * For DragDropQuestion to properly inherit from Question,
 * here inside the DragDropQuestion constructor, we have to
 * explicitly call the Question constructor
 * passing DragDropQuestion as the this object, and the parameters
 * we want to use in the Question constructor:
 * @param theQuestion
 * @param theChoices
 * @param theCorrectAnswer
 * @constructor
 */
function DragDropQuestion(theQuestion, theChoices, theCorrectAnswer) {
  // Call the Question constructor
  Question.call(this, theQuestion, theChoices, theCorrectAnswer)
}

// inherit the methods and properties from Question
DragDropQuestion.prototype = Object.create(Question.prototype)
// Re-assign the constructor
DragDropQuestion.prototype.constructor = DragDropQuestion

Overriding Methods

Overriding methods is another principle of OOP, and we can do it easily with this pattern. Since the Drag and Drop questions will have a different HTML layout from the Multiple Choice questions (no radio buttons, for example), we can override the displayQuestion method, so it operates specifically to the Drag and Drop question needs:

// Override the displayQuestion method it inherited
DragDropQuestion.prototype.displayQuestion = function () {
  let questionToDisplay = this.question
  let choiceCounter = 0

  this.choices.forEach(function (eachChoice)  {
    questionToDisplay += '<div>' + eachChoice + '</div>'
    choiceCounter++
  })

  console.log('html: ', questionToDisplay)
}

In our real Quiz application, we would create a Quiz constructor that is the main application that launches the quiz, but in this article, we can test our inheritance code by simply doing this:

// Initialize some questions and add them to an array
const allQuestions = [
new MultipleChoiceQuestion("Who is Prime Minister of England?", ["Trump", "May", "Löfven", "Macron"], 2),
   
new MultipleChoiceQuestion("What is the Capital of Brazil?", ["São Paulo", "Rio de Janeiro", "Brasília"], 1),
   
new DragDropQuestion("Drag the correct City to the world map.", ["Washington, DC", "Rio de Janeiro", "Stockholm"], 0)
]

// Display all the questions
allQuestions.forEach(function (eachQuestion)  {
    eachQuestion.displayQuestion()
});

This way we could now override the displayQuestion methods and implement specialized version for the MultipleChoice and DragDrop questions.

You can grab the code here


Published: 2019-11-29
Author: Henrik Grönvall
Henrik Grönvall
Copyright © 2022 Henrik Grönvall Consulting AB