OOP

In this section we start looking at Object-Oriented Programming , objects, classes, inheritance, etc.

Objects deep dive

So far we have create basic Objects that contain properties, these properties have four attributes

Changing a properties attribute
let person = {};
Object.defineProperty(person, "name", {
    writable: false,
    value: "Paul"
});

console.log(person.name);                 // "Paul"
person.name = "Will";                     // won't do anything as propery is read-only (not writeable)
console.log(person.name);                 // "Paul"

We can access Object's Properties by using the dot notation or the bracket notation. These are called accessor properties. Javascript introduced Getter and Setters properties, they are basically functions only for which are responsible for getting and setting a value.

Accessor properties example
// Define object with pseudo-private member 'year_' and public member 'edition'
let book = {
    year_: 2017,
    edition: 1
};

Object.defineProperty(book, "year", {      // Year is the property, the block is the accessor properties
    get() {
        return this.year_;
    },
    set(newValue) {
        if (newValue > 2017) {
            this.year_ = newValue;
            this.edition += newValue - 2017;
        }
    }
});

book.year = 2018;
console.log(book.edition);                 // Edition is now 2
Defining multiple properties
// Same as above 
let book = {};
Object.defineProperties(book, {
    year_: {
        value: 2017
    },

    edition: {
        value: 1
    },

    year: {
        get() {
            return this.year_;
        },

        set(newValue) {
            if (newValue > 2017) {
                this.year_ = newValue;
                this.edition += newValue - 2017;
            }
        }
    }
});
Read a property
let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value);                                         // 2017
console.log(descriptor.configurable);                                  // false
console.log(typeof descriptor.get);                                    // "function"

Javascript has the === operator but in some cases it was insufficient, so recently they added Object.is() which behaves the same way but handles these odd cases

Object identity and equality
console.log(Object.is(true, 1));                         // false
console.log(Object.is({}, {}));                          // false
console.log(Object.is("2", 2));                          // false

// Correct 0, -0, +0 equivalence/nonequivalence:
console.log(Object.is(+0, -0));                          // false
console.log(Object.is(+0, 0));                           // true
console.log(Object.is(-0, 0));                           // false

// Correct NaN equivalence:
console.log(Object.is(NaN, NaN)); // true

You can destructure objects easily using the below

Object destructuring
let person = {
    name: 'Paul',
    age: 21
};

let { name: personName, age: personAge } = person;
console.log(personName);
console.log(personAge);

--------------------------------------------------------------------------
// You can take this even further and even use default values if a property is not defined

let person = {
    name: 'Paul',
    age: 21
};

let { name, age, job='Developer' } = person;
console.log(name);
console.log(age);
console.log(job);                                     // Software engineer

Object Creation (ES5)

We have seen how to create objects using the object constructor and the object literal way there are two design patterns that can also be used used, factory pattern and the function constructor pattern (very Java like)

In this section I cover Object creation before ES6 which has more implemented classes and inhertaince, which uses prototyping to create classes and use inheritance.

Factory Pattern
function createPerson(name, age, job) {
    let o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        console.log(this.name);
    };
    return o;
}

let person1 = createPerson("Paul", 21, "Developer");
let person2 = createPerson("Will", 50, "Actor");
Function Constructor Pattern
// The above rewritten as Function Constructor Pattern

// Note the Function name is in UPPER case
// let Person = function(name, age, job) {               // You can also use this way as well for below line
function Person(name, age, job){                             
    this.name = name;
    this.age = age;
    this.job = job;
    
    // this function will be created many times if you create many Person objects
    // once for each object which is a problem with constructor patterns (see below for better option)
    this.sayName = function() {                          
        console.log(this.name);
    };
}

// Note we use the new operator
let person1 = new Person("Paul", 21, "Developer");
let person2 = new Person("Will", 50, "Actor");
Function Constructor Pattern (enhanced)
// The sayName function is only created once and thus multiple Person objects share the same function

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;                     // now points to the function sayName()
}

function sayName() {
    console.log(this.name);
}

Using the prototype pattern each function is created with a prototype property which is a object containing properties and methods that will be available to instances of a reference type. The benefit of a prototype is that its properties and methods are shared will all created instances meaning no additonal resources are created for each object.

Prototype Pattern
let Person = function (name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
}

// This is shared among all created Person objects
Person.prototype.sayName = function() {
    console.log(this.name);
}

let person1 = new Person("Paul", 21, "Developer");
console.log(person1);
person1.sayName();

The Person Object has a prototype object, this object is the same object for all Persons and thus any data (properties/methods) are shared between all the Person Objects created, in our case in the example above we put the function SayName() function which will be shared among all Person Objects.

Inheritance (ES5)

Javascript supports implementation inheritance which means properties and methods will be inherited, prototype chaining is used to implement inheritance, basically the prototype points to another instance of another type this means it has a pointer to another constructor, this can continue to multiple inheritance, you can see how this works in the image below


Inheritance basic example
//Super Class Pet
function Pet() {
    this.name = "";
    this.type = "";
}

Pet.prototype.getPetName = function() {
    console.log("Pets Name: " + this.name);
};

Pet.prototype.talk = function(speak) {
    console.log("Pet talking: " + speak);
};

Pet.prototype.getType = function() {
    console.log("Pet type: " + this.type);
}

// Subclasses Dog and Cat

// Create Dog and inherit from SuperType
function Dog() {
    this.type = "Canine"
}
Dog.prototype = new Pet();

// override existing method and call super method
Dog.talk = function (speak) {
    this.talk(speak);
};

// Create Cat and inherit from SuperType
function Cat() {
    this.type = "Feline"
}
Cat.prototype = new Pet();

// override existing method and call super method
Cat.talk = function (speak) {
    this.talk(speak);
};

// Main

let rover = new Dog();
rover.name = "Rover"
rover.getPetName();
rover.talk("Woof Woof!!");          // example of passing arguments
rover.getType();

let felix = new Cat();
felix.name = "Felix"
felix.getPetName();
felix.talk("Meow Meow!!");          // example of passing arguments
felix.getType();

There is a technique called constructor stealing, basically you call the supertype's constructor from within the subtype's constructor

Constructor Stealing
function SuperType(name){
    this.name = name;
}

function SubType() {
    // inherit from SuperType passing in an argument
    SuperType.call(this, "Paul");

    // instance property
    this.age = 21;
}

let instance = new SubType();
console.log(instance.name);             // "Paul";
console.log(instance.age);              // 21

Lastly on inheritance there are a number of other techniques that can be used, I highlight them here

Combination inheritance
function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
    console.log(this.name);
};

function SubType(name, age){
    // inherit properties
    SuperType.call(this, name);          // constructor stealing
    this.age = age;
}

// inherit methods
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
    console.log(this.age);
};

let instance1 = new SubType("Paul", 21);
instance1.colors.push("yellow");
console.log(instance1.colors);                          // "red,blue,green,yellow"
instance1.sayName();                                    // "Paul";
instance1.sayAge();                                     // 21

let instance2 = new SubType("Will", 50);
console.log(instance2.colors);                          // "red,blue,green"
instance2.sayName();                                    // "Will";
instance2.sayAge();                                     // 50
Prototypal inheritance
let person = {
    name: "Paul",
    friends: ["Will", "Moore", "Graham"]
};

let anotherPerson = Object.create(person);                 // we use the Object.create() method
anotherPerson.name = "Arthur";
anotherPerson.friends.push("Norman");

let yetAnotherPerson = Object.create(person);              // we use the Object.create() method
yetAnotherPerson.name = "George";
yetAnotherPerson.friends.push("Basil");

console.log(person.friends); // "Will, Moore, Graham, Arthur, Basil"
Parasitic inheritance
function createAnother(original){
    let clone = object(original);                          // create a new object by calling a function
    clone.sayHi = function() {                             // augment the object in some way
        console.log("hi");
    };
    return clone; // return the object
}

let person = {
     name: "Paul",
    friends: ["Will", "Moore", "Graham"]
};
let anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"
Parasitic combination inheritance
function inheritPrototype(subType, superType) {
    let prototype = Object(superType.prototype);            // create object
    prototype.constructor = subType;                        // argument object
    subType.prototype = prototype;                          // assign object
}

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
    console.log(this.name);
};

function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
    console.log(this.age);
};


let person1 = new SubType("Paul", 21);
person1.sayName();
person1.sayAge();
console.log(person1.colors);

Classes (ES6)

We take a in depth look at classes which were introduced in ES6, although easier to use behind the scenes they still use prototype and constructor concepts, so understanding the above section is worth while. There are two ways to create a class

Create class
// class declaration
class Person {}

// class expression
const Person = class {};

A class can have constructor, instance variables, instance methods, getter/setter accessor methods and static class methods, this is very similar to Java.

Class example
const Person  = class {

    constructor(name, age) {
        this._name = name  || null;                 // can use defaults
        this._age = age;
    }

    // Getter and Setter accessor methods
    get name() {
        return this._name;
    }

    set name(value) {
        this._name = value;
    }

    get age() {
        return this._age;
    }

    set age(value) {
        this._age = value;
    }

    toString() {
        console.log(this.name + " " + this.age);         // calling the get name() and get age()
    }
    
    // a static method use Person.sayHello() to invoke
    static sayHello() {
        console.log("Hello World!");
    }
}

let person1 = new Person("Paul", 21);
person1.toString();
console.log(person1.name);                                // use the get name() method 

Class inheritance is much much easier than ES5, as it fully supports inheritance in a more friendlier way.

ES6 class basic example
class Pet {
    constructor(name, type) {
        this.name = name;
        this.type = type;
    }

    // Getter & Setter accessor method here, excluded to keep code short

    // inherited by all sub classes
    toString() {
        console.log("Pet name is " + this.name + " and is a " + this.type);
    }
}

class Dog extends Pet {
    constructor(name, type, color) {
        super(name, type);                     // calling super constructor (Pet), should be first command
        this.color = color;
    }

    // Dog class own method
    talk() {
        console.log(this.type + " goes Woof Woof!!");
    }
}

class Cat extends Pet {
    constructor(name, type, color) {
        super(name, type);                     // calling super constructor (Pet), should be first command
        this.color = color;
    }

    // Cat class own method
    talk() {
        console.log(this.type + " goes Meow Meow!!");
    }
}

let rover = new Dog("Rover", "Dog", "Brown");
let felix = new Cat("Felix", "Cat", "Black");

rover.toString();
felix.toString();

rover.talk();
felix.talk();
Abstract Class example (work around)
// ES6 does not fully support Abstract classes but here is a work around

// Abstract base class
class Vehicle {
    constructor() {
        if (new.target === Vehicle) {
            throw new Error('Vehicle cannot be directly instantiated');
        }
        if (!this.foo) {
            throw new Error('Inheriting class must define foo()');
        }
        console.log('success!');
    }
}

// Derived class
class Bus extends Vehicle {
    foo() {}
}

// Derived class
class Van extends Vehicle {}

new Bus(); // success!
new Van(); // Error: Inheriting class must define foo()

One feature with Javascript is that you can extend built-in class as well and you can do multiple inheritance (not fully supported but have a work around)

Inheriting from Built-in types
class SuperArray extends Array {
    shuffle() {
        // Fisher-Yates shuffle
        for (let i = this.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [this[i], this[j]] = [this[j], this[i]];
        }
    }
}

let a = new SuperArray(1, 2, 3, 4, 5);
console.log(a instanceof Array);                                  // true
console.log(a instanceof SuperArray);                             // true
console.log(a);                                                   // [1, 2, 3, 4, 5]
a.shuffle();
console.log(a);                                                   // [3, 1, 4, 5, 2]
Class mixins
class Vehicle {}

let FooMixin = (Superclass) => class extends Superclass {
    foo() {
        console.log('foo');
    }
};

let BarMixin = (Superclass) => class extends Superclass {
    bar() {
        console.log('bar');
    }
};

let BazMixin = (Superclass) => class extends Superclass {
    baz() {
        console.log('baz');
    }
};

function mix(BaseClass, ...Mixins) {
    return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
}

class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}

let b = new Bus();
b.foo();                                // foo
b.bar();                                // bar
b.baz();                                // baz