← Home

JavaScript OOP

  • Encapsulation means putting together a collection of variables and functions as properties and methods in a class.
  • Abstraction means hiding the inner details of a class and exposing only its essential features. This is achieved with private properties and static methods.
  • Inheritance means transferring all of the properties and methods in the parent class to a child class. This prevents code duplication.
  • Polymorphism means overriding a parent class method with a child class method. This way, a method behaves differently based on its calling class.

Creating an object

An object may be created by:

  1. An object literal
const user = {};
  1. The God object
const user = Object.create(null);

Useful for setting the prototype of the newly created object.

  1. A factory function (function returning an object)
// directly returning it

function createCircle(radius) {
    return {
        radius,
        draw: () => console.log("drawing...");
    }
}

// populating it first, returning it at the end:

function userCreator(name, score) {
    const newUser = {};
    newUser.name = name;
    newUser.score = score;
    newUser.increment = function() {
        newUser.score++;
    }
    return newUser;
}

Undesirable repetition! Not DRY.

  1. A constructor function (function with this, to be called with new)
function Circle(radius) {
    this.radius = radius; // member on function itself!
    this.draw = function() { // member on function itself!
        console.log('drawing'...);
    }
}

Note: Avoid using the built-in constructors: String(), Number(), etc. They return an object containing the argument, not a primitive.

  1. A class (syntactic sugar for a factory function)
class Circle {
	constructor(radius) {
		this.radius = radius;
	}
	draw() {
		console.log("drawing...");
	}
}

Factory function

A factory function is a function returning an object. It is named with camelCase and called like a normal function.

function createCircle(radius) {
	return {
		radius, // shorthand for `radius: radius`
		draw: function() {
			console.log("drawing...");
		}
	};
}

let circle1 = createCircle(1);

Constructor function

A constructor function is a function that is used to construct objects, specifically a function that has properties and methods attached to it via this inside the object. By convention, a constructor function is named with PascalCase and is called with the new keyword. A constructor function is creates an object instance.

function Circle(radius) {
    this.radius = radius;
    this.draw = function() {
        console.log('drawing'...);
    };
}

let circle1 = new Circle(1);

Class

A class containing properties and methods, including a constructor function. this refers to the class itself or its instance. A class is named with PascalCase and called with the new keyword.

class Circle {
    constructor(radius) {
        this.radius = radius;
    }
    draw() { // instance method
        console.log("drawing...");
    }

    static parse() { // static method
        console.log("parsing"...);
    }
}

const circle1 = new Circle(1);

Prototypal inheritance

Prototypes

Most programming languages model inheritance with two types of classes:

  • Base, also called “Super” and “Parent”
  • Derived, also called “Sub” and “Child”

In JavaScript, every object has a prototype (its parent object) from which it inherits properties and methods. When accessing a property or executing a method of an object, JavaScript searches for it in the object itself, or walks up the prototype chain to find it. The parent of all objects in JavaScript is ObjectBase.

To get an object’s prototype:

Object.getPrototypeOf(myObject);
myObject.__proto__;

To change an object’s prototype:

Circle.prototype = Object.create(Shape.prototype);
// prototype of `Circle` is now `Shape`

To change an object’s constructor:

Circle.prototype.constructor = Circle;
// Circle's constructor changed to Circle

To change an object’s prototype and constructor simultaneously:

function setParent(Child, Parent) {
	Child.prototype = Object.create(Parent.prototype);
	Child.prototype.constructor = Child;
}

To override a method in an object’s prototype:

Shape.prototype.duplicate = function() {
	console.log("duplicating shape...");
}; // parent method

setParent(Circle, Shape);

Circle.prototype.duplicate = function() {
	console.log("duplicating circle...");
}; // parent method overriden in child

Shared members

For optimization purposes, do not add multiple copies of members in memory.

function Circle(radius) {
	this.radius = radius; // instance property
}

Circle.prototype.draw = function() {
	console.log("drawing..."); // prototype method
};

This way, every time you create an object from Circle(), you do not create an instance of draw(), only a reference to it as a shared method on the constructor’s parent.

circle1; // object
Circle; // constructor
Circle.prototype; // constructor's parent

The for... in loop returns members in both the instance and its prototype.

for (let key in circle1) {
	console.log(key); // radius, draw
}

The Object.keys() method only returns members in the instance.

let x = Object.keys(circle1);
console.log(x); // radius (draw is omitted)

Composition

You can compose properties and methods into objects using mixins.

const canEat = {
	eat: function() {
		console.log("eating...");
	}
};

const canWalk = {
	eat: function() {
		console.log("walking...");
	}
};

Object.assign({}, canEat, canWalk);

Class inheritance

In JavaScript, the class keyword is only syntactic sugar to create a factory function. The extends keyword sets its parent.

Call super() in the constructor to call the parent’s constructor, which allows you to inherit the properties and methods of the parent.

class Shape {
	constructor(color) {
		this.color = color;
	}
}

class Circle extends Shape {
	constructor(color) {
		super(color); // calls parent constructor with value
		this.radius = radius;
	}

	draw() {
		console.log("drawing...");
	}
}

const redCircle1 = new Circle("red", 1);

To override a parent’s class method:

class Shape {
	move() {
		console.log("shape moving...");
	}
}

class Circle extends Shape {
	move() {
		console.log("circle moving...");
	}
}

const circle1 = new Circle();
circle1.move(); // → "circle moving..."

Static methods

Static methods, as opposed to instance methods, are available on the class itself and are used as utility functions not specific to a particular object.

Private members

In constructors

Replace the member that uses the this keyword with a local variable holding the property or method. This way, the property or method using the local variable will not be visible to the outside, i.e., it will be private.

function Circle(radius) {
    let radius = radius; // local variable - private property
    this.draw = function() {
        console.log('drawing'...);
        console.log(radius)
    };
}

In classes

An old convention is using an underscore, but it has no effect in real use. It only indicates to the user that, as a convention, the underscored property is private and not to be accessed from the outside.

To actually privatize a property or method, use Symbols.

const _radius = Symbol();
const _draw = Symbol();

class Circle {
	constructor(radius) {
		this[_radius] = radius; // private property
	}
	[_draw]() {
		// private method
		console.log("drawing...");
	}
}

Or you can use WeakMaps:

const _radius = WeakMap();

class Circle {
	constructor(radius) {
		_radius.set(this, radius);
	}
}

Or you can use the private keyword in TypeScript.

Getters and setters

In constructors

Use getters and setters to get and set properties. This way, you cannot read or change properties unless you specifically use getters and setters.

function Circle(radius) {
    let radius = radius; // local variable - private property
    this.draw = function() {
        console.log('drawing'...);
        console.log(radius)
    };
    Object.defineProperty(this, 'radius', {
        get: function() {
            return radius;
        },
        set: function(value) {
            radius = value;
        }
    }
};

let circle1 = new Circle(radius);
console.log(circle1.radius);
circle1.radius = 2;

In classes

const _radius = WeakMap();

class Circle {
	constructor(radius) {
		_radius.set(this, radius);
	}
	get radius() {
		return _radius.get(this);
	}

	set radius(value) {
		_radius.set(this, value);
	}
}