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'susername
,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"
// }
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.
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
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!
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.
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.
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,
});
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 ourCreateNewUser
; greetingMessage()
andfarewall()
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.
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());
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);
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 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
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());