background

Object Composition and Prototypal Inheritance in JavaScript

The usual way to write 'classes' in JavaScript is to write a constructor function which would be used in conjunction with the new keyword to create and return a new object.

You can also inherit from these constructor functions to create more specialised objects, almost as you would expect to be able to do in any other language.

I say almost because in JavaScript classes do not exist, only objects exist which other objects are based on.

For example:

// Create our base constructor function
function Animal() {
  // Set a default value for the cry member
  this.cry = '...';
}

// Add a method that will be common to all Animal objects
Animal.prototype.talk = function talk() {
  console.log(this.cry);
};

// Create a new constructor function to be used
function Cat() {
  // Override the cry member with Meow for this animal
  this.cry = 'Meow';
}

// Add another specific method for Cat objects
Cat.prototype.purr = function() {
  console.log('Purr');
};

// Inherit from Animal which will give Cat the ability to call the talk() method
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

// Create a new cat from the Cat constructor function
var cat = new Cat();

// Talk
cat.talk();

In most cases this will work without any problems when you first write it, but as a project evolves eventually some requirements will surface that force you to change one of the parents of a hierarchy, or worse, the root of the hierarchy.

Even a small change can cause a lot of work, testing and re-factoring of code.

For example if we decided to extend Cat into different types of cats:

// Create another constructor function for a Lion
function Lion() {
  // We need to override the cry
  this.cry = 'Roar! I am a Lion :D';
}

// And inherit from Cat
Lion.prototype = new Cat();
Lion.prototype.constructor = Lion;

All is working well and our objects are inheriting nicely until we have a requirement to add in a new MuteCat object which forces us to override both the talk() and purr() methods.

// Create another constructor function for the lowly MuteCat
function MuteCat() {}

// Inherit from Cat, since we don't want to confuse the `instanceof` operator
// That is, if you ever use `instanceof`... Duck Typing is much more reliable
// in JavaScript, especially across execution contexts
MuteCat.prototype = new Cat();
MuteCat.prototype.constructor = MuteCat;

// We have to override the purr() and the talk() method
MuteCat.prototype.talk = function() {};
MuteCat.prototype.purr = function() {};

This doesn't seem like much of a problem now, but as the project grows and more cats are added we end up with more and more slightly different classes, making it more work to change methods like purr() in the future.

After a while we end up with objects that are holding more functionality than is actually required, or even slightly different objects that only exist for very specific cases.

A better way to approach object creation is to compose objects from a selection of other objects, mixing and matching only what you need using factories.

This isn't to say that using classical inheritance is bad, since it does work when you know the design won't change much.

Using factories also has the nice effect of showing exactly what the JavaScript is doing. Since prototype oriented languages, such as JavaScript, don't have classes the constructor pattern confuses things a bit.

It makes it look like the constructor function is a class, when it is in fact just a normal object where the new keyword creates an object for you and sets the context of this to the new object.

Creating objects via composition means that common public methods can stay on a prototype using delegation, state can be stored on instances using mix-ins and encapsulation can be achieved with the use of closures.

Essentially you get everything that the constructor pattern gives you but in a much more flexible way.

For example:

// Some general functions that we can slot into any prototype object we want
var talk = function talk() {
  console.log(this.cry || '...');
};

var purr = function purr() {
  console.log('Purr');
};

// This should be defined outside of the factory
var catPrototype = {
  state: {
    cry: 'Meow' // A default state
  },
  methods: {
    talk: talk,
    purr: purr
  },
  closures: [
    function() {
      var _id = 0;

      this.setId = function setId(id) {
        _id = id;
      }

      this.getId = function getId() {
        return _id;
      }
    }
  ]
};

// Our factory
var createCat = function createCat() {
  // Set proto to reference the actual prototype object we want to use
  var proto = catPrototype;

  // Create a new instance of an object using proto.methods as the prototype
  // This means the common methods for cats will all delegate saving memory
  var obj = Object.create(proto.methods);

  // Loop the closures and call with `this` set to the new instance
  proto.closures.forEach(function(closure) {
    closure.call(obj);
  });

  // Simple copy of state to the new instance
  for (var key in proto.state) {
    obj[key] = proto.state[key];
  }

  return obj;
};

// Create a couple of cats
var cat1 = createCat();
var cat2 = createCat();

// We have private instance safe variables because of the closures
console.log(cat1.getId());
cat1.setId(1);
console.log(cat1.getId());

// We have common public methods are delegated to the object's prototype
cat1.talk();
cat1.purr();

// We have instance safe state
cat1.cry = 'Roar!';
cat1.talk();

// Proves all of the above
console.log(cat2.getId());
cat2.setId(2);
console.log(cat2.getId());
cat2.talk();
cat2.purr();

Object.getPrototypeOf(cat1).purr = function() {
  console.log('Proof of delegation');
};

cat2.purr();

Now we could create another factory called createMuteCat which wouldn't need the talk() or purr() methods.

Because we are creating objects by composition rather than tightly coupling them with a pseudo-classical inheritance, we can mix and match our object prototypes and make changes easily without having to worry about the overall hierarchy so much.

We could develop this further to allow for defaults and options to be passed into the factory, allowing for overrides and object use without having to initialise values manually.

This method is of course not without it's drawbacks, and while this method of creating objects is a bit more verbose it certainly makes things a lot easier in general once you get used to it.