JavaScript Inheritance without ES6 classes

The ES6 class syntax helps you create classical Inheritance patterns. However there are no class types in JavaScript. Only objects.

Consider a simple class definition in ES6:

class Animal {
constructor(name) {
this.name = name || '';
}
breathe() {
console.log(`${this.name} Huffs`)
}
}
const pig = new Animal('Babe');
console.log(pig.name);
pig.breathe();

There are a several articles on the web that explains the problems with classical inheritance. So we will not repeat the brittleness of it or the banana gorilla story here.

In JavaScript, class is syntactic sugar and not an equivalence to classes in other languages such as Java. Thus teaching JavaScript engineers the nature of constructs such as class, extends, constructor, super, new, typeof and this is an overhead and often confuses new developers. In my opinion it is useless to learn class based development in JavaScript. The good news is that there are better ways to write excellent javascript with out using these keywords.

Here is the equivalent Class using just functions and objects. Typically you would define a factory function:

const getAnimal = (name) => {
let properties = { name };
return ({
get name() { return properties.name },
set name(newName) { properties.name = newName },
breathe: () => console.log(`${name} Huffs`)
})
}
const wolf = getAnimal("The Wolf");
console.log(wolf.name);
wolf.breathe();

Just knowing how JavaScript objects work, you can understand this easily. If the accessor methods are new, I recommend you get familiar with get and set:

Try this example out here: https://codepen.io/rajeshnaroth/pen/MdwowY

JavaScript Class(ical) Inheritance

You should always favor composition over classical inheritance.

https://web.archive.org/web/20170628213618/http://www.advogato.org/article/83.html

Classical inheritance enforces a rigid taxonomy. Modeling real world using taxonomies becomes brittle as you start accommodating exceptions. In fact the real world only has very shallow is-a relationship. Composition lets you create a has-a relationship. Composition behaviors lets you create a can-do relationship. Let us consider a class based classical inheritance example.

class Animal {
constructor(name) {
this.name = name || '';
}
breathe() {
console.log(`${this.name} Huffs`)
}
}
class Amphibian extends Animal {
constructor(name) {
super(name);
}
swim() {
console.log(`${this.name} Swims.`);
}
}
const frog = new Amphibian("pepe");
frog.swim();
frog.breathe();

JavaScript Prototypal inheritance

function Animal(name) {
this.name = name;
this.breathe = function() {
console.log(`${this.name} breathes.`);
}
}
function Amphibian(name) {
this.name = name;
this.swim = () => console.log(`${this.name} Swims.`);
}
Amphibian.prototype = new Animal("");
const froggie = new Amphibian("froggie");
froggie.swim();
froggie.breathe();

A lot of wiring going on here. You have to understand the behavior of this, prototype and new before understanding what the piece of code does. The code readability is very poor.

JavaScript Functional Inheritance

The term Functional Inheritance was coined by Douglas Crockford and explained in the book “Javascript — The Good Parts”.

const getAnimal = (name) => {
let properties = { name };
return ({
get name() { return properties.name },
set name(newName) { properties.name = newName },
breathe: () => console.log(`${name} Huffs`)
})
}
const getAmphibian = (name) => {
const animal = getAnimal(name);
animal.swim = () => console.log(`${animal.name} swims`);
return animal;
}
const kermit = getAmphibian("Kermit");
kermit.swim();
kermit.breathe();

Can it be that simple? Yes. You’ve inherited Animal by simply creating an instance and extending its properties. Try it at: https://codepen.io/rajeshnaroth/pen/MdwowY

If all you have is simple one to tow two level Inheritance categorical hierarchies. This works great.

JavaScript Concatenative Inheritance

How do you solve the Platypus effect? In the JavaWorld, you can “program to an interface”. Essentially composing multiple behaviors into one class. In JavaScript you can simply concatenate the behavior. Let us start with a simple base class

const Animal = (name) => {
let properties = { name };
return ({
get name() { return properties.name },
set name(newName) { properties.name = newName },
breathe: () => console.log(`${name} Huffs`)
})
}

Now we will define a few behaviors. These behaviors can also contain its own properties.

const aquaticKind = (animal) => ({
swim: () => console.log(`${animal.name} swims`)
})
const walkingKind = (animal, noOfLegs) => {
const properties = { noOfLegs }
return ({
get noOfLegs() { return properties.noOfLegs },
set noOfLegs(n) { properties.noOfLegs = n; },
walk: () => console.log(`${animal.name} walks with ${properties.noOfLegs} legs`)
})
}
const eggLayingKind = (animal) => ({
layEgg: () => console.log(`${animal.name} laid an egg`)
})

Now we can mix and match the behaviors to create our animals. I’m going for specific animals here but you can as well create classifications by picking and choosing behaviors.

Crocodile

const Crocodile = (name) => {
const animal = Animal(name);
return Object.assign(animal,
walkingKind(animal, 4),
aquaticKind(animal),
eggLayingKind(animal)
);
}
const snooty = Crocodile('snooty');
snooty.swim();
snooty.walk();
snooty.name = "coolie";
snooty.swim();
snooty.walk();
snooty.layEgg();

Ape

const Ape = (name) => {
const animal = Animal(name);
return Object.assign(animal,
walkingKind(animal, 2)
);
}
const ape = Ape('Caesar');
ape.walk();

Platypus

const Platypus = (name) => {
const animal = Animal(name);
return Object.assign(animal,
eggLayingKind(animal, 4),
aquaticKind(animal)
);
}
const platypus = Platypus('Platypus');
platypus.swim();
platypus.layEgg();

The secret sauce here is Object.assign that concatenates all the objects together.

Things to keep in mind

Object.assign() only does a shallow copy. If your objects contain nested properties, do a deep copy using Ramda’s clone() or Lodash’s cloneDeep().

Mixins is another name for concatenative inheritance. Name clashing is an obvious problem with combining objects. But modeling your data is a deliberate effort usually within the same team and you should never have name conflicts.

Mixins make a copy of the “parent” object every time. If your objects are heavy, be wary of its memory impact.

Try the examples here: https://codepen.io/rajeshnaroth/pen/WBwPBZ

Frontend Architect, San Jose, CA