JavaScript Prototypical Inheritance simplified with a Class module

03 Feb 2016 21:07
Tags inheritance

Back to list of posts

The Problem

I was recently working with Phaser to create a game and needed to derive new classes found in the Phaser library. The Phaser library (and most libraries available today for that matter) implements Prototypical inheritance. Prototypical inheritance in JavaScript is often confusing for developers experienced in class-based languages. In JavaScript, there are no classes, only objects. Each object is inherited from another object. I find the syntax to be cumbersome to work with and wanted to find a better way to implement inheritance.

Overview of Prototypical Inheritance

Creating an Object

A constructor in JavaScript is just a function. Methods are added to the object by assigning functions to the object's prototype. A new instance of the object is created using new. Here is an example:

// Person constructor.
function Person(name) {
  this.name = name;
}
 
// Person.getName function.
Person.prototype.getName = function () {
  return this.name;
};
 
// Create instance of Person.
var p = new Person("John");
 
// Get the person's name.
console.log(p.getName());

Properties

The syntax for defining properties for an object is also confusing and therefore isn't used as much as it could be used. To define a property, you use the Object.defineProperty function and pass in the object's prototype, name of the property and an object describing the property. Here are a couple of examples of defining a property for Person:

Object.defineProperty(Person.prototype, "name", {
  get: function () {
    return name;
  },
  set: function (value) {
    name = value;
  }
});
 
Object.defineProperty(Person.prototype, "age", {
  value: 0,
  writable: true,
  enumerable: true,
  configurable: true
});
 
// Create instance of Person.
var p = new Person("John");
 
// Get the person's name
console.log(p.name);
 
// Set the person's age
p.age = 40;
 
// Get the person's age
console.log(p.age);

Inheritance

Inheriting from another object in JavaScript uses the Object.create function. A function is defined for the constructor of the derived object. Then the parent object prototype is passed into the Object.create function to create a new object that is assigned to the prototype of the derived object before any methods or properties are defined for the derived object. Finally, the constructor for the derived object prototype is set to the constructor function that was initially defined. Here is an example of an Employee object derived from the Person object:

// Define a constructor for the derived object.
var Employee = function (name, employer) {
  Person.call(this, name);
  this.employer = employer;
}
 
// Inherit from the parent object.
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
 
// Define methods for the derived object.
Employee.prototype.getEmployer = function () {
  return this.employer;
};

Creating the Class module

Instead of defining objects using the JavaScript syntax for Prototypical Inheritance, I created a Class module to make inheritance easier to implement. The Class module exposes 2 functions, define and extend. the define function is used to define a parent object and the extend function is to extend an existing object. Additionally, the Class module has a _defineMethodsAndProperties function that takes the definition passed into the define or extend functions and adds all the properties and methods to the object.

_defineMethodsAndProperties

A JSON object is just a dictionary of key/value pairs. The _defineMethodsAndProperties function loops over the keys defined in the JSON object, and depending on what the value is, will create either a method, property with get and set functions, writable property or constant property. If the value is a function, then the function is added to the object prototype. If the value is an object with a get and/or set function then a property is defined. If the value is null, then a writable property is defined. And finally, if the value is a not null and not a function, then a property is defined that is not writable. Here is the code:

function _defineMethodsAndProperties (cls, def) {
  // Identify the methods and properties.
  for (var key in def) {
    if (typeof def[key] === "function") {
      // This is a method.
      cls.prototype[key] = def[key];
    } else if (def[key] !== null && (typeof def[key].get === "function" || typeof def[key].set === "function")) {
      // This is a property.
      Object.defineProperty(cls.prototype, key, def[key]);
    } else if (def[key] === null) {
      // This is a member variable.
      Object.defineProperty(cls.prototype, key, {
        value: null,
        writable: true,
        configurable: true,
        enumerable: true
      });
    } else {
      // This is a constant.
      Object.defineProperty(cls.prototype, key, { value: def[key] });
    }
  }
};

define

The define function has one parameter that is a JSON object. The JSON object contains the following:

  • Member Variables - writable member variables are defined as null and constant member variables are defined with a value.
  • Constructor - a constructor function is defined with a key of "constructor" and a function as the value.
  • Properties - a key with a value that is a JSON object containing a get and/or set key with a function as the value.
  • Methods - a key with a function as a value.

If a constructor is found in the JSON object passed in, then the constructor is used to define the object. If a constructor is not found, then a default constructor is created for the object. The methods and properties are added to the object by calling the _defineMethodsAndProperties function and finally an extend method is added to the object definition. Here is the code:

function define (def) {
  /// <summary>Define a new object.</summary>
  var cls = def.constructor || Object.create(Object.prototype);
 
  _defineMethodsAndProperties(cls, def);
 
  cls.extend = function (def) {
    return extend(cls, def);
  };
 
  // Return the class definition.
  return cls;
}

extend

The extend function has two parameters: the parent object and the definition of the derived object. The definition of the derived object is a JSON object similar in definition passed into the define function. If a constructor is found in the JSON object that was passed in, then the constructor is used to define the object. If not then a default constructor is defined that calls the parent constructor. The derived object is then inherited from the parent object with the Object.create function. Then the methods and properties are added to the object by calling the _defineMethodsAndProperties function. Here is the code:

function extend (parent, def) {
  /// <summary>Extend the object with additional methods and properties.</summary>
 
  var cls = null;
 
  if (def.constructor === Object) {
    // The class doesn't have a constructor, so create a constructor
    // that calls the parent constructor.
    cls = function () {
      parent.apply(this, arguments);
    };
  } else {
    // Use the class constructor.
    cls = def.constructor;
  }
 
  cls.prototype = Object.create(parent.prototype);
  cls.prototype.constructor = cls;
 
  _defineMethodsAndProperties(cls, def);
 
  return cls;
};

Using the Class module

Define a parent object

Here is an example of defining a Person object using the Class.define function:

var Person = Class.define({
  name: null,
  birthday: null,
 
  constructor: function (name, birthday) {
    this.name = name;
    this.birthday = birthday;
  },
 
  age: {
    get: function () {
      var ageDifMs = Date.now() - this.birthday.getTime();
      var ageDate = new Date(ageDifMs);
      return Math.abs(ageDate.getUTCFullYear() - 1970);
    }
  },
 
  sayHello: function () {
    return "Hello, my name is " + this.name;
  }
});

Now that the object is defined, a new instance of the object can be created with the new keyword. Here is an example of creating a new Person object:

var p = new Person("John", new Date("1/1/1970"));
console.log(p.age);
console.log(p.sayHello());

Extend the parent object

Since the Person object was defined using the Class module, the Person object has an extend function. The extend function is a wrapper around the Class.extend function and passes itself as the parent to the Class.extend function. When calling the Person.extend function, all that needs to be passed in is the definition of the derived object. Here is an example of deriving from the Person object to create an Employee object:

var Employee = Person.extend({
  employer: null,
  constructor: function (name, birthday, employer) {
    Person.call(this, name, birthday);
    this.employer = employer;
  },
  sayHello: function () {
    // Call the parent class sayHello function.
    var greeting = Person.prototype.sayHello.call(this);
    greeting += ". I work for " + this.employer;
    return greeting;
  }
});

Here is an example of creating a new Employee object:

var e = new Employee("John", new Date("1/1/1970"), "Acme Inc.");
console.log(e.sayHello());

Extend an object that was not defined with Class.define

If a class was not defined with the Class.define function, then the extend function was not added to it. However, the Class.extend function can still be used to derive from an existing object.

Here is an example of extending the Array object to create a StringBuilder object:

var StringBuilder = Class.extend(Array, {
  constructor: function (value) {
    if (typeof value !== "undefined") {
      this.push(value.toString());
    }
  },
  append: function (value) {
    this.push(value.toString());
  },
  appendLine: function (value) {
    this.push(value.toString() + "\r\n");
  },
  toString: function () {
    var result = "";
    for (var idx = 0; idx < this.length; idx++) {
      result += this[idx];
    }
    return result;
  }
});

Here is an example of creating a new instance of a StringBuilder object:

var sb = new StringBuilder("Hello");
sb.appendLine(", my name is John.");
sb.append("Nice to meet you.");
console.log(sb.toString());

jsFiddle

The JavaScript Inheritance with Class module Fiddle demonstrates the use of the Class module.

Downloads

class.js


Comments: 0

Add a New Comment

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License