logo

Object-oriented programming in JavaScript

February 5, 2021

In JavaScript there are two types of constructors:

  • Built-in constructors like Array, Date, String, Number, Boolean and many more;
  • Custom constructors functions that we can create and use to define and initialize objects and their properties and methods.

Let's create a new function called createNewUser:

function createNewUser(username, name, age, country) {
  const obj = {};
  obj.username = username;
  obj.name = name;
  obj.age = name;
  obj.country = country;
  obj.greetingMessage = function () {
    console.log("Welcome back " + obj.name + "!");
  };
  return obj;
}

You can now create a new person by calling the createNewUser function:

const bob = createNewUser("bob2020", "Bob", 20, "Germany");
console.log(bob.username); // bob2020
console.log(bob.name); // Bob
console.log(bob.greetingMessage()); // Welcome back Bob!

Let's try to refactor the createNewUser function by using a constructor function.

function CreateNewUser(username, name, age, country) {
  this.username = username;
  this.name = name;
  this.age = age;
  this.country = country;
  this.greetingMessage = function () {
    console.log("Welcome back " + this.name + "!");
  };
}

If we compare the above two functions we will notice some differences:

  • The constructor function name starts with a capital letter. It's a convention to capitalize the name to distinguish from the regular functions;
  • You can also notice that the constructor function does not return anything and does not explicitly create a new object;
  • The keyword this is being used as well. When an object instance is created, the object's username, name, age, country properties will be equal to the values passed to the constructor call.

Let's start creating new objects:

const user1 = new CreateNewUser("bob2020", "Bob", 20, "Germany");
const user2 = new CreateNewUser("marye23", "Mary", 17, "Netherlands");

user1.username; // "bob2020"
user1.name; // "Bob"
user1.age; // 20
user1.country; // "Germany"
user1.greetingMessage(); // Welcome back Bob!

user2.username; // "marye23"
user2.name; // "Mary"
user2.age; // 17
user2.country; // "Netherlands"
user2.greetingMessage(); // Welcome back Mary!

Let's go back to the constructor calls. The new keyword is used to tell the browser we want to create a new object instance, followed by the function name, in our case the CreateNewUser, and it's required parameters. The result is stored in a variable.

const user1 = new CreateNewUser("bob2020", "Bob", 20, "Germany");
const user2 = new CreateNewUser("marye23", "Mary", 17, "Netherlands");

After the objects have been created, the user1 and user2 variables contain the following objects.

Important Note: When we are calling our constructor function, we are defining greeting() every time, which is not quite ideal. Instead, we should define functions on the prototype.

{
  username: "bob2020",
  name: "Bob",
  age: 20,
  country: "Germany",
  greetingMessage: function () {
    console.log("Welcome back " + this.name + "!");
  }
}

{
  username: "marye23",
  name: "Mary",
  age: 17,
  country: "Netherlands",
  greetingMessage: function () {
    console.log("Welcome back " + this.name + "!");
  }
}

There are other ways to create objects instances:

  • Using the Object() constructor or
  • Using the create() method.

Using the Object() constructor

The Object constructor can be used to create a new object and then you can add properties and methods to the object by using the dot notation or bracket notation.

let user3 = new Object();
user3; // {}
user3.username = "debbie2";
user3["name"] = "Debbie";
user3.age = 32;
user3["country"] = "Iceland";
user3.greetingMessage = function () {
  console.log("Welcome back " + this.name + "!");
};

When you add properties to an object, it is essential to be consistent and use the dot notation or bracket notation, not both at the same time.

You can also pass in an object literal to the Object() constructor as a parameter:

let user4 = new Object({
  username: "lunya_1",
  name: "Lunia",
  age: 19,
  country: "Denmark",
  greetingMessage() {
    console.log("Welcome back " + this.name + "!");
  },
});

Using the create() method

JavaScript allows you to create a new object based on an existing object. Now, try typing in your console user5.name. You will notice that the user5 has been created based on user2 and both have the same methods and properties.

create() method under the hood is creating a new object using user2 as a prototype object. You can check this by entering user5.__proto__ in the console.

let user5 = Object.create(user2);
console.log(user5.__proto__); // it will return the user2 object
// {
//   "username": "marye23",
//   "name": "Mary",
//   "age": 17,
//   "country": "Netherlands"
// }

Creating a new object based on existing object

Now, let's dive into Object prototypes

Object prototypes

JavaScript is often described as a prototype-based language, which means that every single JavaScript object has a property called prototype, which points to the object prototype.

Objects use the object prototype to inherit properties and methods defined on the prototype property on the Objects constructor function.

A quick reminder: an object is any value that is not a primitive (a string, a number, a boolean, a symbol, null or undefined). Arrays and functions are, under the hood still objects.

Let's get back to our CreateNewUser constructor and our object instance we created:

function CreateNewUser(username, name, age, country) {
  this.username = username;
  this.name = name;
  this.age = age;
  this.country = country;
  this.greetingMessage = function () {
    console.log("Welcome back " + this.name + "!");
  };
}

const user1 = new CreateNewUser("bob2020", "Bob", 20, "Germany");

If you type user1 in the console, the browser tries to auto-complete with the properties and methods available on this object.

age, country, greetingMessage, name, username are defined on the CreateNewUser constructor, but there are other methods available for us to use - but those are defined on the Object constructor.

Methods available on the object instance

We can call the valueOf() even if we didn't define it:

user1.valueOf();
// {
//   "username": "bob2020",
//   "name": "Bob",
//   "age": 20,
//   "country": "Germany"
// }

The valueOf() method is defined on the Object and it returns the value of the object it is called on.

valueOf() is inherit by user1 because its constructor is CreateNewUser(), and CreateNewUser()'s prototype is Object.

The browser initially checks to see if user1 object has the valueOf() method defined on its constructor, CreateNewUser. Its constructor does not have the method, and it goes up on the prototype chain and checks to see if the CreateNewUser constructor's prototype object, which is Object(), has a valueOf() method available on it. Since it does it returns the value of the object it is called on.

If the method does not exist it will return an error, like in the below example, the someMethod method does not exist on CreateNewUser nor Object neither.

user1.someMethod();
// Uncaught TypeError: user1.someMethod is not a function

Object-instance-inheritance-prototype

By using Object.getPrototypeOf(user1) we can access the prototype of the user1 object.

The constructor property

The constructor property points to the original constructor function.

console.log(user1.constructor); // returns the CreateNewUser constructor
console.log(user2.constructor); // returns the CreateNewUser constructor
// function CreateNewUser(username, name, age, country) {
//   this.username = username;
//   this.name = name;
//   this.age = age;
//   this.country = country;
//   this.greetingMessage = function () {
//     console.log("Welcome back " + this.name + "!");
//   };
// }

We can also create a new object instance from the CreateNewUser constructor by using:

let user6 = new user1.constructor("dino2", "Dinor", 10, "Austria");
console.log(user6);
// {
//   "username": "dino2",
//   "name": "Dinor",
//   "age": 10,
//   "country": "Austria"
// }

We can also use it find the name of the constructor it is an instance of:

user6.constructor.name; // "CreateNewUser"

Adding methods to the constructor's prototype

We can add methods to the constructor's prototype:

CreateNewUser.prototype.farewall = function () {
  console.log("Logging out " + this.name + "!");
};

let user7 = new CreateNewUser("bubi", "Bubin", 20, "Austria");

user1.farewall(); // Logging out Bob!
user7.farewall(); // // Logging out Bubin!

Adding methods to the constructor's prototype

As you can see the whole inheritance chain has been updated making the farewall method available on all object instances created from the CreateNewUser constructor.

Creating an object that inherits from another object

Let's use again our CreateNewUser constructor function, but this time we will define the methods on the constructor's prototype.

function CreateNewUser(name, username, email, password) {
  this.name = name;
  this.username = username;
  this.email = email;
  this.password = password;
}

CreateNewUser.prototype.greetingMessage = function () {
  console.log("Welcome back " + this.name + "!");
};

CreateNewUser.prototype.farewall = function () {
  console.log("Logging out " + this.name + "!");
};

Let's define a AssignRights() constructor function which will inherit the properties and methods from the CreateNewUser constructor function and also add some properties and methods to it.

function AssignRights(
  name,
  username,
  email,
  password,
  roleName,
  permissionLevel
) {
  CreateNewUser.call(this, name, username, email, password);
  this.roleName = roleName;
  this.permissionLevel = permissionLevel;
}

We need further to make AssignRights() constructor function to inherit the methods defined on CreateNewUser() prototype.

constructor function without reference

We need to create a new object and assign it to AssignRights.prototype. The new object needs to have the CreateNewUser.prototype in order for AssignRights.prototype to inherit all the methods and properties available on the CreateNewUser.prototype.

AssignRights.prototype = Object.create(CreateNewUser.prototype);

If we now check the constructor property for both of them we will both points to the CreateNewUser() constructor which is not what quite good.

Constructor inheritance problem

To fix it, we will modify the constructor property of the AssignRights.prototype and update its value to AssignRights.

Object.defineProperty(AssignRights.prototype, "constructor", {
  value: AssignRights,
  enumerable: false,
  writable: true,
});

Update the AssignRights reference

AssignRights.prototype.showPermissionLevel = function () {
  console.log(
    this.username +
      " is a " +
      this.roleName +
      " and the permission level is " +
      this.permissionLevel
  );
};

let user1 = new AssignRights(
  "Carla",
  "carla99",
  "carla99@gmail.com",
  "21Y9xtG",
  "Designer",
  3
);

user1.showPermissionLevel(); // carla99 is a Designer and the permission level is 3
user1.greetingMessage(); // Welcome back Carla!

Rewriting the code and using the class syntax

The class statement indicates that we are creating a new class. Inside of it, we define:

  • The constructor method used to define the constructor function that represents our CreateNewUser;
  • greetingMessage() and farewall() which are the class methods.

Important Note: Under the hood, the CreateNewUser class is converted into Prototypal Inheritance models.

To create AssignRights subclass, making it inherit from CreateNewUser class, we need to use the extends keyword.

By using the extends keyword, we tell JavaScript that the class we want to base our class on is CreateNewUser.

When using the old constructor function syntax, the new keyword does the initialization of this to a newly allocated object.

The problem is when we use the extends keyword, because this is not automatically initialized when a class is defined by the extend keyword.

This is where the super operator comes into place. super must be used before the this and we pass in the necessary arguments of the CreateNewUser class constructor in order to initialize the parent class properties in our subclass, and therefore will inherit them.

Now, when we instantiate AssignRights object instances we can call methods and properties defined on both CreateNewUser and `AssignRights.

class CreateNewUser {
  constructor(name, username, email, password) {
    this.name = name;
    this.username = username;
    this.email = email;
    this.password = password;
  }
  greetingMessage() {
    console.log(`Welcome back ${this.name}!`);
  }
  farewall() {
    console.log(`Logging out ${this.name}! Good bye ${this.name}!`);
  }
}

const tom = new CreateNewUser("Tom", "ctommy", "c.tommy@gmail.com", "******");

tom.name; // "Tom"
tom.greetingMessage(); // Welcome back Tom!
tom.farewall(); // Logging out Tom! Good bye Tom!

class AssignRights extends CreateNewUser {
  constructor(name, username, email, password, roleName, permissionLevel) {
    super(name, username, email, password);
    this.roleName = roleName;
    this.permissionLevel = permissionLevel;
  }
  showPermissionLevel() {
    console.log(
      `${this.username} is a ${this.roleName}, permission level: ${this.permissionLevel}`
    );
  }
}

let bobby = new AssignRights(
  "Bobby",
  "b0b0",
  "bobby0@gmail.com",
  "******",
  "Project Manager",
  2
);

Working with Getters and Setters

Getters and setters work in pairs. A getter returns the current value of variable and setter changes the value of the corresponding variable to the one it defines.

In our class below, we have a setter and getter for the roleName property. In order to create a separate value in which to store our roleName property we use _, which is a convention. If we don't use it we will get errors every time we call get or set.

class AssignRights extends CreateNewUser {
  constructor(name, username, email, password, roleName, permissionLevel) {
    super(name, username, email, password);
    this._roleName = roleName;
    this.permissionLevel = permissionLevel;
  }
  get roleName() {
    return this._roleName;
  }
  set roleName(newRole) {
    this._roleName = newRole;
  }
}

let joana = new AssignRights(
  "Joana",
  "joannam",
  "joannam@gmail.com",
  "******",
  "Intern",
  4
);
joana.roleName; // "Intern"
joana.roleName = "Designer";
joana.roleName; // "Designer"

From ES5 to ES6 a practical example

function User(name, age, favColor) {
  console.log("User constructor was initialized...");
  this.name = name;
  this.age = age;
  this.favColor = favColor;
  this.displaySummary = function () {
    return `${this.name} is ${this.age} years old and her favorite color is ${this.favColor}`;
  };
}

// We create an user from the `User` constructor
// When we instantiate an object from the constructor, the code inside of the `User` constructor will be executed

const user1 = new User("Claire", 20, "magenta");
console.log(user1);
// User constructor was initialized...
// {
//   "name": "Claire",
//   "age": 20,
//   "favColor": "magenta"
// }
console.log(user1.displaySummary()); // Claire is 20 years old and her favorite color is magenta

const user2 = new User("Mary", 10, "yellow");
console.log(user2.name);
console.log(user2.displaySummary());

When we are calling our constructor function, we are defining displaySummary() every time, which is not ideal. Instead, we should define it on the prototype.

displaySummary is defined on every instance

function User(name, age, favColor) {
  this.name = name;
  this.age = age;
  this.favColor = favColor;
}

// We store the summary in the prototype since we don't want it for each user
User.prototype.displaySummary = function () {
  return `${this.name} is ${this.age} years old and her favorite color is ${this.favColor}`;
};

const user1 = new User("Claire", 20, "magenta");
console.log(user1);
console.log(user1.displaySummary());

const user2 = new User("Mary", 10, "yellow");
console.log(user2);
console.log(user2.displaySummary());

displaySummary is defined on the prototype

function User(name, age, favColor) {
  this.name = name;
  this.age = age;
  this.favColor = favColor;
}

User.prototype.displaySummary = function () {
  return `${this.name} is ${this.age} years old and her favorite color is ${this.favColor}`;
};

// create an `Information` object that inherits the properties of the `User` constructor
function Information(name, age, favColor, fullName, petName) {
  User.call(this, name, age, favColor);
  this.fullName = fullName;
  this.petName = petName;
}

// Instantiate `Information` Object
const info1 = new Information("Claire", 20, "magenta", "Claire Stokes", "Dino");
console.log(info1.displaySummary()); // Uncaught TypeError: info1.displaySummary is not a function

// We need to inherit the prototype method on the `User`:
Information.prototype = Object.create(User.prototype);
const info2 = new Information("Mary", 10, "yellow", "Moore", "Mars");
console.log(info2);

// Use the `Information` constructor instead of `User`:
Information.prototype.constructor = Information;
console.log(info2);

Use the Information constructor

We can create objects using Object.create():

const userProto = {
  dispaySummary: function () {
    return `${this.name} is ${this.age} years old and her favorite color is ${this.favColor}`;
  },
};

// Create the object
const user1 = Object.create(userProto);
user1.name = "Claire";
user1.age = 20;
user1.favColor = "magenta";

console.log(user1);

const user2 = Object.create(userProto, {
  name: { value: "Mary" },
  age: { value: 10 },
  favColor: { value: "yellow" },
});

console.log(user2);

Using Object.create

Using classes and subclasses:

class User {
  constructor(name, age, favColor) {
    this.name = name;
    this.age = age;
    this.favColor = favColor;
  }
  dispaySummary() {
    return `${this.name} is ${this.age} years old and her favorite color is ${this.favColor}`;
  }
  static someContent() {
    return "Hello there!";
  }
}

const user1 = new User("Claire", 20, "magenta");
console.log(user1); // The constructor is `User`, under the hood is using prototypes

user1.someContent(); // Uncaught TypeError: user1.someContent is not a function

console.log(User.someContent()); // we need to run `someContent()` method on the actual class
// `someContent()` is a method on the `User` class that can be use without instantiating an object

Using classes

class User {
  constructor(name, age, favColor) {
    this.name = name;
    this.age = age;
    this.favColor = favColor;
  }
  dispaySummary() {
    return `${this.name} is ${this.age} years old and her favorite color is ${this.favColor}`;
  }
}

class Information extends User {
  constructor(name, age, favColor, fullName, petName) {
    // `super` is used in order to call the parent constructor and we want to pass to it the original parameters
    super(name, age, favColor);
    this.fullName = fullName;
    this.petName = petName;
  }
}

// Instantiate the `Information` subclass
const info1 = new Information("Claire", 20, "magenta", "Claire Stokes", "Dino");
console.log(info1);

// calling `dispaySummary` method
console.log(info1.dispaySummary());

Using subclasses