JS - OOPs

aigle-levant · September 7, 2024

We shall look into what an object is in JS…

Object

Refer -> My notes on OOP, in case you don’t know what an object is and notes on data types in JS

Objects are the representation of real-world objects in the virtual world. In JS, they belong to a type called object.

Creating an object

Objects can be created in 4 ways :

  • Using an object initializer [or object literals]
  • Using new
  • Using a factory function
  • Using a constructor

Object literals

Object is a collection of key-value pairs. These pairs represent an object’s properties [in other languages, we call them instance variables].

If you’ve dabbled in python before, you must be familiar with dictionaries. What we’re going to do now resembles them.

Let’s say I’m making a character for Project Zomboid…

const player = {
    name: "Jean Antonique",
    job: "Ranger",
    traitsStengths: ["Outdoorsman", "Axe Man", ""],
    traitsWeakness: ["Thin-skinned", "Fear of Blood", ""],
    //...
    mainWeapon: "Axe"
};

Let’s try printing the object :

console.log(player);

/*
{
  name: 'Jean Antonique',
  job: 'Ranger',
  ...
  mainWeapon: 'Axe'
}
*/

Aww man! We don’t need the EXACT thing we declare just moments ago!

To avoid this problem, we simply use the object’s keys using an operator called dot operator [.] :

let str = `Name: ${player.name}\nJob: ${player.job}`;

console.log(str);
/*
Name: Jean Antonique
Job: Ranger
*/

This operator lets you access the instance variables of an object.

We can also access them using objectName['propertyName']. This method’s used when a property consists of many words that are separated by space [or if they’re enclosed by single quotes].

const player = {
    //...
    'traits strengths': ["Outdoorsman", "Axe Man", ""],
    'traits weakness': ["Thin-skinned", "Fear of Blood", ""],
    //...
};

console.log(player['traits strengths'] + '\n' + player['traits weakness']);

/*
[ 'Outdoorsman', 'Axe Man', '' ]
[ 'Thin-skinned', 'Fear of Blood', '' ]
*/

Factory functions

Object literals are great if we’re dealing with a single object. Not so great if we deal with stuff like this that involves duplication :

const player = {
    name: "Jean Antonique",
    job: "Ranger",
    traitStrength: ["Outdoorsman", "Axe Man", ""],
    traitWeakness: ["Thin-skinned", "Fear of Blood", ""],
    jacket: "Wedding Suit",
    shirt: "Dress Shirt",
    pant: "Dress Pants",
    //...
    mainWeapon: "Axe"
};

const playerTwo = {
    name: "Yves Montagne",
    job: "Burger Flipper",
    traitStrength: ["Resilient", "Amateur Mechanic", ""],
    traitWeakness: ["Sunday Driver", "Conspicuous", ""],
    jacket: "Flannel",
    shirt: "Vest",
    pant: "Jeans",
    //...
    mainWeapon: "None"
};

Remember the programmer’s adage - DRY : Don’t Repeat Yourself; don’t duplicate unless necessary.

They even share the same properties, to our chagrin. We need some way to ease our workload. What shall we do?

We use factory functions :

function Player (playerName="Anonymous", playerJob="Unemployed", playerStrength, playerWeakness, playerJacket="None", playerShirt="Dress Shirt", playerPant="Jeans", playerWeapon="None")
{
    return {
        name: playerName,
        job: playerJob,
        traitStrength: playerStrength,
        traitWeakness: playerWeakness,
        jacket: playerJacket,
        shirt: playerShirt,
        pant: playerPant,
        //...
        mainWeapon: playerWeapon,
        playerInfo : function()
        {
            console.log(
                //prints arguments provided
            );
        }
    }
}

This allows us to create object as per our wish.

const playerOne = Player("Jean Antonique", "Ranger", "Outdoorsman", "Fear of Blood", "Wedding Suit", "Dress Shirt", "Dress Pants", "Axe");

playerOne.playerInfo()

Pro tip

Now I’ll let you know a cool trick - if the key name and the value name [which comes from the function’s parameters] are the same, we can get rid of the value name and just keep the key name alone :

function Player (playerName="Anonymous", job="Unemployed", strength, weakness, jacket="None", shirt="Dress Shirt", pant="Jeans", mainWeapon="None")
{
    return {
        playerName, job, strength, weakness, jacket,
        shirt, pant, mainWeapon,
        playerInfo : function()
        {
            console.log(
                //prints arguments provided
            );
        }
    }
}

Constructors

Another way is by using constructors. This word must be familiar to those who know Java or Python - yes, it’s a way to create instances of a class. However, JS doesn’t have the concept of classes; we’ll have to settle with objects alone instead.

function Player(playerName="Anonymous", playerJob="Unemployed", playerStrength, playerWeakness, playerJacket="None", playerShirt="Dress Shirt", playerPant="Jeans", playerWeapon="None")
{
    this.playerName = playerName;
    this.job = job;
    //...
    this.mainWeapon = this.mainWeapon;

    this.playerInfo = function()
    {
        //print the player's information
    }
}

const playerOne = new Player("Jean Antonique", "Ranger", "Outdoorsman", "Fear of Blood", "Wedding Suit", "Dress Shirt", "Dress Pants", "Axe");

Wow, it does resemble a Java class, complete with the this keyword and private instance variables!

Well, to those not in the know, this refers to the object involved here. We use it to point to the object instead of point at the program as a whole, allowing us to hide away the complexities of a program.

Constructor functions for other types

Whenever we use the object literal method to create an object, JS uses a function called Object() to construct it.

Same goes for any other type in JS. Don’t believe me? Then check this out :

let song = new String("Take Me to Church");
let premier = new Number(1);
let ifElse = new Boolean(true);

console.log(song, premier, ifElse);
/*
[String: 'Take Me to Church']
[Number: 1]
[Boolean: true]
*/

This is a cumbersome way of defining a string, number, Boolean, etc. literal. We’d rather use our original way of doing things.

Adding properties

We can add properties on the fly to our object. To show this, let me add the player’s starting location :

playerOne.location = "Rosewood";

console.log(playerOne.location); //Rosewood

We can also do this using bracket notation. I’ll add a confirmation for laceration wound :

playerOne['laceration head'] = true;
console.log(playerOne['laceration head']); //true

Deleting properties

Now that we know how to add things, we should also know how to delete things. I’ll delete the player’s weapon property here :

delete playerOne.playerWeapon;
console.log(playerOne);
/*
Player
{
    playerName: 'Jean Antonique',
    job: 'Ranger'
}
*/

Iterating over properties

We are finished with our edits. Let’s print out the player’s information :

function Player(parameters)
{
    return {
        playerName, job, strength, weakness, weapon, jacket, shirt, pant,
        playerInfo : function()
        {
            console.log
            (`
                Name: ${playerName}
                Job: ${job}
                Strength: ${strength}
                Weakness: ${weakness}
                Jacket: ${jacket}
                Shirt: ${shirt}
                Pant: ${pant}
                Weapon: ${weapon}
            `)
        }
        playerOutfit : function()
        {
            console.log(`
            ${playerName} is currently wearing a ${jacket}, underneath which is a ${shirt}, and ${pants}.
            `);
        }
    }
}
console.log(playerOne.playerInfo());
console.log(playerOne.playerOutfit());

What a lengthy code. It’s tiring to see through all this fluff. We could and should use a loop instead to print the properties :

for (let i in playerOne)
{
    console.log(`${i} : ${playerOne[i]}`);
}
/*
playerName : Jean Antonique
job : Ranger
strength : Outdoorsman
weakness : Fear of Blood
weapon : Axe
jacket : Wedding Suit
shirt : Dress Shirt
pant : Dress Pants
playerOutfit : function()
{
    console.log(`${playerName} is currently wearing a ${jacket}, underneath which ia ${shirt}, and ${pants}.`);
}
*/

We’ve solved one problem and got to another : we don’t need the method here, how do we remove it?

We use a conditional :

for (let i in playerOne)
{
    if (typeof playerOne[i]!=='function')
    {
        console.log(`${i} : ${playerOne[i]}`);
    }
}

Obtaining keys and values

Like I said before, Object is another data type. We can access it using the dot operator [or notation] to perform various actions.

Object.keys(), for instance, returns the keys of an object :

console.log(Object.keys(playerOne));

/*
['playerName','job','strength','weakness','weapon',
  'jacket','shirt','pant','playerOutfit']
*/

And in the same vein, Object.values to return the values :

console.log(Object.values(playerOne));
/*
['Jean Antonique','Ranger','Outdoorsman','Fear of Blood','Axe','Wedding Suit','Dress Shirt','Dress Pants',
[Function: playerOutfit]
]
*/

What’s this [Function: playerOutfit] you ask? It is the value we obtain when our object has a method inside it. It normally looks like [Function: functionName].

What if we want both the keys and the values to appear together? We use Object.entries() :

console.log(Object.entries(playerOne));
/*
[
  [ 'playerName', 'Jean Antonique' ],
  [ 'job', 'Ranger' ],
  [ 'strength', 'Outdoorsman' ],
  [ 'weakness', 'Fear of Blood' ],
  [ 'weapon', 'Axe' ],
  [ 'jacket', 'Wedding Suit' ],
  [ 'shirt', 'Dress Shirt' ],
  [ 'pant', 'Dress Pants' ],
  [ 'playerOutfit', [Function: playerOutfit] ]     
]
*/

Abstraction

When we say abstraction, we mean hiding away the implementation of our object [or class] and showing only the essentials.

In a microwave, you have on the inside the circuit board, heating mechanism, etc. and buttons on the outside. We can only access its functions using the allowed actions [the buttons].

Say, we want to create a program to resemble a bank. We’d ask our user for their account number and then create an object based on it.

function banque(accountNumber)
{
    this.accountInfo = function()
    {
        return (accountNumber);
    }
    //withdraw()
    //deposit()
}

const input = prompt("Enter your account number: ");
const account = new banque(input);

Now let’s start adding functionality to our object. Let’s introduce a balance property :

function banque(accountNumber)
{
    this.accountNumber = accountNumber;
    this.balance = 0;

    this.setBalance = function(amount)
    {
        if (amount>0)
        {
            this.balance = amount;
        }
    }
    this.accountInfo = function()
    {
        return (`
            Account: ${this.accountNumber}
            Balance: ${this.balance}
            `);
    }
    //withdraw()
    //deposit()
}

When we use the dot operator with our account object, the following appear :

alt text

This is not good. We shouldn’t allow our dearest, most private properties to be handled by the user.

We have to demote them to local, alas :

function banque(accountNumber)
{
    let balance = 0;

    this.setBalance = function(amount)
    {
        if (amount>0)
        {
            balance = amount;
        }
    }
    this.getBalance() = function()
    {
        return balance;
    }
    this.accountInfo = function()
    {
        return (`
            Account: ${accountNumber}
            Balance: ${getBalance()}
            `);
    }
    //withdraw()
    //deposit()
}

Here I’ve gotten rid of the accountNumber variable and added a getter function [function that gets something]. This allows us to simply use getBalance() in situations where you’ll need the balance.

Helps us keep our code encapsulated [neatly arranged with getters and setters].

Getting and setting properties

Another way we can get properties is by our old friend, Object type.

Prototype

A prototype is an object that other objects in JS inherit properties and methods from.

In short, it’s a way to share properties and methods among instances.

Let’s say we’ve a function inside our factory function called info() that returns the information about a book. If this function were defined on the prototype object, our other objects can also access it, thanks to inheritance.

Okay, but why do we need prototypes?

Here are 2 things to consider :

  • Defining properties and functions directly into the prototype saves a lot of memory. It’s better than defining every single property and function.
  • Prototypal inheritance : Objects that we create inherit from constructorName.prototype object to access functions like info().