Object-oriented Programming in JavaScript

Romeo Agbor Peter - Jun 16 '21 - - Dev Community

In programming, there is a technique for writing code called object-oriented programming. It's a methodology for code abstraction and organization where the code written embodies the traits of real-world objects (like a car, a house or even a person) and their related features. Although each programming languages differ in the implementation of OOP, the concept remains the same.

Encapsulation

In object-oriented programming, programs are divided into pieces, and each piece is responsible for managing its state. The way a piece of program works is local and encapsulated to that piece. This is known as encapsulation.

Different pieces of the program interact with each other through interfaces --- functions or bindings that provide interaction, and useful functionalities at an abstract level, hiding their precise implementation. Such programs are modelled after objects, again, like a car, a house or a person.

An object consists of properties (attributes and methods), properties that are part of an object, and only work within that object are private. Others, that part of the object, but interact with outside code are called public.

In JavaScript, there is no distinct way to identify private, and public properties, and to prevent outside code from accessing the private properties. One common way is to describe properties that are private or public in the documentation and comments. Another way is to use an underscore (_) at the start of property names to indicate that they are private.

Method

Methods are property names in an object that hold function values.

For instance, a simple method:

const pet = {};
pet.bark = (bark) => {
    console.log(`My dog says '${bark}'`);
}

pet.bark("Woof") // → My dog says 'woof'
Enter fullscreen mode Exit fullscreen mode

Methods are called on object to perform certain tasks. When a function is invoked as a method, the binding this points to the object that the method was called on.

For instance:

function speak(line) {
    console.log(`The ${this.position} person says '${line}'`);
}

const firstHomie = {position: "first", speak};
const secondHomie = {position: "second", speak};

firstHomie.speak("Yo man, wats happenin?");
// → The first person says 'Yo man, wats happenin?'

secondHomie.speak("All good dawg!");
// → The second person says 'All good dawg!'
Enter fullscreen mode Exit fullscreen mode

The this can be called using a function method called call. The call method takes the value this as its first argument, other arguments are treated as normal parameters.

speak.call(secondHomie, "Good to know."); // → The second person says 'Good to know.'
Enter fullscreen mode Exit fullscreen mode

NOTE:

In JavaScript, the "this" always binds or points to the object, and can be used to reference the object.

Regular function defined with the function keyword can't refer to the this of a wrapping scope. such a function can only use its own this binding. Arrow functions don't work in the same way. An arrow function is able to access the this binding of the scope around it.

for instance, reference this from inside a local function:

/* Using normal function */
function normalize () { // → wrapping scope
    console.log(this.coords.map(function(n){
        n / this.length
    }));
}
normalize.call({coords: [0, 2, 3], length: 5});
// → Undefinded values [ undefined, undefined, undefined ]

/** Using arrow function **/
function normalize () {
    console.log(this.coords.map(n => n / this.length));
}
normalize.call({coords: [0, 2, 3], length: 5});
// → [0, 0.4, 0.6]
Enter fullscreen mode Exit fullscreen mode

Prototypes

In JavaScript, most objecst can inherit properties from a parent object or a prototype. When an object is requested to access a property it does not have, it'll look into its prototype, if that prototype doesn't have it, then the prototype's prototype will be checked and so till it gets to the ancestor prototype.

let emptyObject = {};
console.log(emptyObject.toString());
// → [object Object]
Enter fullscreen mode Exit fullscreen mode

In the above, the object is able to access a method that is not part of its property, but part of a fallback prototype that most object inherit from -- the ancestral object prototype, Object.protoptype.

You can check the prototype of an object using the object constructor's getProtoypeOf method:

//...

console.log(Object.getPrototypeOf(emptyobject));
// → {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ,…}
Enter fullscreen mode Exit fullscreen mode

The getProtoypeOf method returns the ancestral prototype properties. Object.prototype provides a few default methods, like toString(), that shows up in all object.

Some JavaScript object don't inherit from Object.prototype as their prototype, but from another object that provides a set of default values. Date inherit from Date.prototype, arrays from Array.prototype, and functions from Function.prototype.

You can use the create method from the object constructor to create an object with its unique prototype.

let protoMouse = {
    speak(line) {
      console.log(`The ${this.type}Mouse says ${line}`);
  }
}

let dangerMouse = Object.create(protoMouse);
dangerMouse.type = "DANGER";
dangermouse.speak("Pooww!")
// → The DANGER mouse says Poow!
Enter fullscreen mode Exit fullscreen mode

From the code above, the dangerMouse object has a default property of speak from its fallback object protoMouse and a property of type that applies only to itself. The protoMouse object can be used as a container for all mice. You could have "ninja mouse", "nerdy mouse", "tech mouse" and so on. Each mouse object can implement its property but all share the same prototype, protoMouse.

JavaScript Classes

Before the class template was introduced in JavaScript, the language used a prototype in implementing OOP classes.

A class is an OOP concept for defining the properties (methods and attributes) of an object in a structured manner. It's the blueprint of an object. Objects derived classes are an instance of that class.

Prototypes can be used for defining properties from which all instances of a class can inherit (share properties). Dissimilar or different properties per instance, like type, have to be defined in the object.

To create an instance of a class, you first create an object that inherits from the proper prototype. The object has to have the properties that instances of that class are supposed to have. That is what constructor functions do.

//...

function makeMouse(type) {
    let mouse = Object.create(protoMouse);
    mouse.type = type;
    return mouse;
}

// Instance of 'makeMouse' class
let ninjaMouse = makeMouse("Ninja");

ninjaMouse.speak("Haiiyahh!");
// → The NinjaMouse says Haiiyahh!
Enter fullscreen mode Exit fullscreen mode

The code above is one way of doing it, but there is an easier way: using the new keyword.

When a function is prepended with the new keyword, it creates and automatically returns an empty object that is bounded to the this keyword. The object has a prototype property derived from Object.prototype.

Let's create a different class this time:

function Person(first, last, age, eye) {
    this.firstName = first;
    this.lastName = last;
    this.age = age;
    this.eyeColor = eye;
}

// All instances will share this property
Person.prototype.speak = functin(line) {
    console.log(`${this.firstName} says '${line}'`);
}

// An instance of the 'Person' class
let teacher = new Person("Romeo", "Peter", 22, "black");
let student =  new Person("Jane", "Doe", 25, "brown");

console.log(teacher.name); // → Romeo
console.log(student.name); // → Jane

teacher.speak("hello world!"); // → Romeo says 'hello world!'
student.speak("hello!"); // → Jane says 'hello!
Enter fullscreen mode Exit fullscreen mode

NOTE:

It is considered good practice to name constructor functions with an upper-case first letter.

Because constructors are functions, the actual prototype of the function is Function.prototype.

Class template

A class template is a constructor function with a prototype property. It allows for a much simpler way of writing classes in JavaScript. The feature is part of the ES2015 update to JavaScript.

class Person {
    constructor(first, last, age, eye) {
        this.firstName = first;
      this.lastName = last;
      this.age = age;
      this.eyeColor = eye;
    }

    speak(line) {
        console.log(`${this.firstName} says '${line}'`);
    }
}

// Instances of 'Person' class
let teacher = new Person("Romeo", "Peter", 22, "black");

teacher.speak("hello world!"); // → Romeo says 'hello world!'
Enter fullscreen mode Exit fullscreen mode

The class keyword is used to start or declare the class, it's followed by two curly braces {}. you can declare any number of method within the class that'll be part of the prototype, but the constructor is special and must come first. The constructor method is a construction function that will be bounded to the class name.

NOTE:

It is considered good practice to name class template with an upper-case first letter.

You can compare the previous class declaration that used a function to this current, one and you'll see that they're similar. Using the class template is a simpler way to write read classes.

Although class template can only hold methods (function values) for now, it is a better approach to using classes. A future update to JavaScript might allow for other values to be stored within class templates.

Polymorphism

Polymorphism means "many forms."

In computer programming, polymorphism refers to data or object that can be used or processed in more than one form. This is a key part of OOP because it allows instances of a class to be any form as long as the expected interfaces or data types are provided.

For instance, we described what form a person can by declaring the Person class. Consequently, a person can be a Father, Mother, Daughter or Son.

// ...

// Instances of 'Person' in more than one form
let father = new Person("John", "Doe", 30, "black");
let Mother = new Person("Jane", "Doe", 25, "brown");
let daughter new Person("Gill", "Doe", 3, "black");
let son = new Person("Jack", "Doe", 3, "brown");
Enter fullscreen mode Exit fullscreen mode

Another example, although more technical, is the JavaScript String() method that converts a value to a string. It is a polymorphic code that expects a certain interface to function as it should -- convert a value to a string.

When a String() method is called on an object, it'll calls the toString() method on that object and then converts it to a string. the method (String()) expect the object to have the toString() method as an interface.

It's possible to overwrite the toString() method in the prototype. Let's do that by creating the string to be returned by the String() method.

// ...

Person.prototype.toString = function() {
    return `Teacher's name is ${this.firstName}.`;
}

console.log(String(teacher)); // → Teacher's name is Romeo.
Enter fullscreen mode Exit fullscreen mode

Polymorphic code can work with data values of different forms, as long they provide the data types or interfaces required.

Getters, Setters and Static

Getters and setters allow you to read and write to an object expression or class declaration. These are properties that have hidden method calls.

You define a getter by using the keyword get in front of a method in an object expression or class declaration.

For instance, a class for getting varying sizes:

// Object expression
let varyingSize = {
    get size() {
        return Math.floor(Math.random() * 100);
    }
}
console.log(varySize.size) // → 12

//-----------------------------------

// Class declaration
class VarifyingSize {
    get size() {
        return Math.floor(Math.random() * 100);
    }
}
let size = new verifyingSize();
console.log(size.size); // → 61
Enter fullscreen mode Exit fullscreen mode

from the code above, when you read the object's or class' size property, it calls the associated method. Similarly, you can write into an object or class by defining a setter.

For instance, a temperature class that sets to Fahrenheit:

class Temperature {
    constructor(celsius) {
        this.celsius = celsius;
    }

    get fahrenheit() {
        return this.celsius * 1.8 + 32;
    }

    set fahrenheit(value) {
       this.celsius = (value - 32) / 1.8;
    }
}

let temp = new Temperature(40);

// Get
console.log(temp.farenheit); // → 104

// Set
temp.fahrenheit = 86;
console.log(temp.celcius) // → 30
Enter fullscreen mode Exit fullscreen mode

Static methods, when set, are attached (stored implicitly) to the class constructor and don't have access to the class instances. That means they are methods set for a class and not for the instance of that class. Such a method can be used to provide additional ways of creating instances. You define a static method by using the static keyword in front of the method.

class Temperature {
  //...

    // Store on the class constructore
    static fromFahrenheit(value) {
        return new Tempareture((value 32) / 1.8);
    }
}

// Create temperature using degrees Fahrenheit
Tempareture.fromFahrenheit(100);
// → Temperature {celsius: 37.77777777777778}
Enter fullscreen mode Exit fullscreen mode

Inheritance

Inheritance in OOP is when a class extends another class.

When you inherit from a class, you create new functionalities and features on top of the existing one.

// Parent or super class
class Animal {
    constrcutor(name) {
        this.name = name;
        this.speed = 0;
    }

    run(speed) {
        this.speed = speed;
        console.log(`${this.name} runs with speed ${this.speed}.`);
    }

    stop() {
        this.speed = 0;
      console.log(`${this.name} stands still.`);
    }
}

//...
Enter fullscreen mode Exit fullscreen mode

The above code is a generic class for animals. A generic animal like a horse can run and stop. To create a horse object, the object would have to extend the Animal class. The extends keyword is used to achieve that. The keyword extends tells the class not to directly derive from the default Object.prototype but from a class

class Horse extends Animal {
    hide () {
        alert(`${this.name} hides!`);
    }
}

let horse = new Horse("Black Stallion");
horse.run(120) // → Black Stallion runs with speed 120.
horse.hide("neigh") // → Black stands hides!.
horse.stop() // → Black stands still.
Enter fullscreen mode Exit fullscreen mode

JavaScript's prototype makes it possible to derived properties from one class to another. The top class knows as the Parent class, shares its properties with the bottom, known as the child class. The child class can define its properties, like a method.

NOTE:

Any expression is allowed after extends

Class syntax allows to specify not just a class, but any expression after extends.

For instance, a function call that generates the parent class:

function f(phrase) {
return class {
sayHi() { alert(phrase); }
};
}

class User extends f("Hello") {}

new User().sayHi(); // Hello

Here class User inherits from the result of f("Hello").

That may be useful for advanced programming patterns when we use functions to generate classes depending on many conditions and can inherit from them.

Source: JavascriptInfo

By default, all methods not specified in the child class are inherited from the parent class. For instance, the stop() method in the previous example is derived from the parent class. If the same method is specified in the child class it'll overwrite the parent's method. Properties in the child class should only extend or build on top of the parent class. To avoid overwriting methods, a child class should call parent class properties (methods and constructor) with the super keyword.

For instance, the horse will autohide when stopped.

class Horse extends Animal {
    hide () {
        alert(`${this.name} hides!`);
    }

    stop() { // Child class method. Does not overwrite parent
        super.stop(); // Call parent 'stop()' method
        this.hide(); // hide
    }
}
Enter fullscreen mode Exit fullscreen mode

The horse class above has a stop method that calls the parent's stop method underneath in the process.

Classes that extend another class with no constructor method automatically generate an "empty" constructor. If no explicit constructor method is written, it calls the parent constructor and passes all the arguments in.

class Horse extends Animal {
    // generated for extending classes without own constructors
    constructor(...args) {
        super(...args)
    }
}
Enter fullscreen mode Exit fullscreen mode

To add a custom constructor to child classes, the constructor must call super() before using this. This is because the derived constructor can't access the object for this if the parent constructor is not called first.

class Horse extends Animal {
    constructor(name, sound) {
        super(name);
        this.sound = sound;
    }

    //...
}

sound() {
    console.log(`The ${this.name} ${this.sound}'s.`)
}

let horse  = new Horse("Black Stallion", "neigh")

console.log(horse.name) // → Black Stallion
horse.sound() // → The Black Stallion neigh's.
Enter fullscreen mode Exit fullscreen mode

Inheritance is a fundamental part of object-oriented programming. It allows building on top of existing data. However, unlike encapsulation and polymorphism that allow for code separation into pieces, thus reducing the overall entanglement, inheritance link and tie code together, thus increasing the tangle. When inheriting from class, knowing when to use it is just as important as how to use it.

Summary

A recap of what we've covered so far.

Object-oriented programming is a methodology for code organization by representing data objects.

Encapsulation allows pieces of code to manage their state, and interact with other code through an interface.

In JavaScript, properties that operate within an object are private while properties that interact with outside code are public.

There are two ways to distinguish public and private properties:

  • Specify in documentation public and private properties.
  • Use underscore _ in front of the property that is private.

Prototypes are fallback objects that an object can inherit from. Most built-in objects inherit from the ancestral Object.prototype object.

Classes are the blueprint for an object. Objects derived from classes are instances of the class.

The class template was introduced in ES2015 and is a constructor function with a prototype property. It allows for a simpler way of describing classes

Polymorphism allows for objects to be used in more than one way.

Getters & setters allow to read and write to an object or class declaration.

Inheritance allows for an extension of classes. The class that extends to another class is known as the child class and the class that is extended from is known as the parent class.

. . . . . . . . . . . . . . . . . . .