In this section we start looking at Object-Oriented Programming , objects, classes, inheritance, etc.
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 |
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.
![]() |
![]() |
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); |
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 |