Object-Oriented Programming in JavaScript. Basics, definitions, and examples of design patterns.
As a new concept, OOP in JavaScript can be a challenge to put your mind around. It took me over two months of procrastination to feel a good grasp of abstractions and their implementations. In this article, I will describe:
- OOP design patterns,
- Tools available to craft these designs, and
- Examples of their implementation.
OOP designs encapsulate states and functionalities, provide a public interface and minimize duplication through inheritance. These designs help programs easier to debug and maintain. Even though proper encapsulation may reduce dependencies, some cases may not lead to more efficient code as they may take more memory space.
- Encapsulation — bundling the state (properties) and behavior (methods) into an object.
- Public interface — accessible functionalities and data of objects available for use without exposing the implementation details.
Ex:length
property of the Array.prototype returns the length of an array when called upon an instance like['a', 'b', 'c'].length //3
- Inheritance — delegating objects' method calls and property look-up to the prototype chain. For example, array instances inherit from
Array.prototype
. That’s why a method['z', 'b', 'c'].sort()
will be delegated toArray.prototype
.
Ex:['a', 'b', 'c'].hasOwnProperty(1) //true
- Dependencies — When manipulations of one section impact another section of the program. In other words, when one part, or function, depends on another. This causes a domino effect when editing or debugging, making code vulnerable to errors.
OLOO — Objects Linking Other Objects
Regular factory functions are great for creating many objects of the same type; however, some methods and static properties endure a significant amount of duplication in the background. OLOO solves the concern by making use of prototypes and, instead of duplicating, delegating methods and properties up the prototype chain.
- Factory function— a function that returns an object literal. It is useful when creating multiple objects with the same properties. The downside to this pattern is a duplication of methods in the background nor can we tell the “type” of the created object. that can be avoided using prototypal inheritance.
- Prototypal inheritance — an inheritance where the internal [[Prototype]] property is set to another object, allowing the inheriting object to access properties and methods. Note that, in JavaScript, an object can inherit only from one prototype.
A convenient way to set up the OLOO pattern is to
- Create an object literal as a prototype
- Define an initializer method
init
for instance properties - Use
Object.create(proto)
to create an instance of the object
let catPrototype = {
type: 'cat',
speak: function () {},
init: function (name, age) {
this.name = name;
this.age = age;
return this;
},
};
let tom = Object.create(catPrototype).init('Tom', 3);
// Object.create creates a new instance of catPrototype,
// .init assigns instance properties and returns the object
// an instance gets assigned to a variable tom
let fury = Object.create(catPrototype).init('Fury', 1);
// Object.create creates a new instance of catPrototype,
// .init assigns instance properties and returns the object
// an instance gets assigned to a variable fury
Pseudo-Classical Design using Constructors and Inheritance
JavaScript does not have a Class as a separate data type. When JavaScript developers refer to a class, they are referring to a constructor function. Hence the name — pseudo-classical.
Pseudo-classical inheritance uses constructors’ built-in prototype
property that references an object. When the constructor is invoked, the returned instance has its internal [[Prototype]]
property set to reference the same object referenced by the constructor’s prototype
property.
The trick here is not to equate object prototype with constructor prototype property:
- [[Prototype]] or prototype—a reference to the parent, or supertype, from which an object can inherit methods and properties. In JavaScript, an object can inherit only from one prototype.
- Constructor — a function that is designed to return an instance using the invocation with a
new
keyword. - Instance — an object created by the constructor.
- Constructor prototype property —a built-in property of a constructor that references the prototype of its instances.
No worries, it’s simple. I’ll repeat it a few times until it’s clear. Let’s do an example,
function Cat (name, age) {
this.name = name;
this.age = age;
}
Cat.prototype.speak = function () {
return 'Myow!';
};
let tom = new Cat('Tom', 3);
let fury = new Cat('Fury', 1);
The constructor’s
.prototype
property IS NOT its prototype, it is a[[Prototype]]
of its instances.
To prove the point, here are some logs that use the previous code. Feel free to run them in your browser or REPL.
// The constructor property refers to the function that created the object
console.log(Cat.constructor); // Function
console.log(tom.constructor); // Cat
// `type` property belongs to the prototype of `tom` but not of `Cat`
// Constructor DOES NOT inherit from its `.prototype` property
// The instance of the constructor inherits from the constructor's prototype
console.log(tom.sound()); // 'Myow!'
console.log(Cat.sound()); // TypeError: Cat.speak is not a function
// Constructor Cat's prototype property IS NOT its prototype,
// Its [[Prototype]] is Function.prototype
console.log(Cat.prototype); // {speak: function(){}}
console.log(Object.getPrototypeOf(Cat) === Function.prototype); // true
// The `type` property is in a hidden [[Prototype]] of an instance
// It can be accessed by a dunder proto (depricated) or Object.getPrototypeOf()
console.log(tom.prototype); // undefined
console.log(tom.__proto__); // {speak: function(){}}
console.log(Object.getPrototypeOf(tom)); // {speak: function(){}}
console.log(Cat.prototype === Object.getPrototypeOf(Cat)); // false
console.log(Cat.prototype === Object.getPrototypeOf(tom)); // true
Why the drama or How to add a subtype?
Using the previous code, let’s add a subtype by linking the prototype of all instances to the prototype of the supertype:
function Cheshire (name) {
// reusing Cat constructor for the same properties (lazy or smart?)
Cat.call(this, name, 'immortal');
}
console.log(Cheshire.prototype.constructor); // Cheshire
Cheshire.prototype = Object.create(Cat.prototype);
let cheshire = new Cheshire ('Cheshire');
console.log(cheshire.sound()) // 'Myow!';
// Prototype chain would look like this:
// cheshire ---> Cheshire.prototype ---> Cat.prototype ---> Object.prototype ---> null
In Object.create(prototype)
, the argument is a prototype of a newly created object. A side effect of the method is that the default constructor property Cheshire.prototype.constructor
is overridden to Cat
; however, Cheshire
is the actual constructor that we use to create instances. It’s important to reset the constructor to Cheshire
for the correct, specific reference. For instance, how would a Cheshire breeder feel if you say that she just breeds Cats? Be specific, and no feelings would get hurt.
Best practice: Good labels make our lives easier.
console.log(Cheshire.prototype.constructor); // Cat
Cheshire.prototype.constructor = Cheshire;
ES6 Class Sugarcoating
class
is sweet. It’s a constructor function with a little blemish.
class Cat {
constructor(name, age){
this.name = name;
this.age = age;
}
}
Cat.prototype.sound = function () {
return 'Myow!';
}
class Cheshire extends Cat {
constructor(name) {
super(name, 'immortal');
}
}
let tom = new Cat('Tom', 3);
let cheshire = new Cheshire('Cheshire');
- No commas!
- Instance state properties go to the constructor method within the class
- Static properties (on the constructor object) need a keyword
static
- Superclass is assigned with
extends
keyword - Call
super
to invoke the supertype constructor or access its arguments
It’s scrumptious! Do I need to say anything else?
Lil Extra on Dependencies
Found it a struggle myself, to understand where to put what methods and properties. If you’re having a hard time with classes and you’re hack-slashing (or copy-pasting) to try to make the RPS, TTT, or 21 games work, take an abstraction step back to see what dependencies you have.
Encapsulation
— Are the interactions and properties making sense?
— Do these functionalities belong to a subclass or superclass?
Verbs and nouns draft in the beginning help a lot. I found it helpful to think in the following sequence: who is who, who has what, who does what, do does how. It’s very important not to get bogged down by the details of implementation before the structure is complete. Plan first, code last.
Mix-ins
— Do unrelated classes sometimes share functionality?
If two things are unrelated, don’t weave a thread through method arguments to get to a method in a sub-sub-sub class of a sister branch in inheritance. Pull the method out into a mix-in, and conveniently sprinkle it onto a Christmas cake and a car wrap design.
Functional or Object-Oriented Programming
— Did the methods in your code get really lengthy?
A function or a method should do one thing, be it a side-effect or an output.