Wednesday, April 16, 2014

Javascript Prototypical Inheritance Done Right

This post has moved! Click here to view the new home of this post.

There are a lot of confusing, misleading or outright incorrect tutorials on how to do prototypical inheritance in ECMAScript (commonly known as Javascript). In this post I will explain how to perform inheritance in a memory-efficient and fast manor, while making all of the language tools work correctly.

Introduction

In this post I will not talk about the other forms of "inheritance" commonly used. I will focus on prototypical inheritance. This form of inheritance provides many features over other forms but also has some downsides as listed below. However I feel that the downsides are very minor, and the largest one (private data must be accessible) is going away quite soon (in ES6) which I will explain in a later post. In most cases I would recommend this form of inheritance.

Features

  • Each method is a single function, which uses less memory and increases the likelihood that it is jit'ed. Plus it only has to be parsed and jit'ed once.
  • Simple and straight foreword.
  • Works with builtin operators such as instanceof.

Downsides

  • Requires private data to be accessible (currently).
  • Methods need to be bound before being passed around.

Creating a Base Class

Okay, let's get started. Creating a base class is simple. It is just a function which modifies the this object. Most people who are familiar with javascript probably know the first few steps like the back of their hand. For now I will just prefix private variables with an underscore, but I will discuss better hiding methods in a future post.

function Mob(name, health) {
 this.name   = name;
 this._health = health || 100;
}

Then you can create and instance as follows.

var john = new Mob("John");
assert(john._health === 100);

The new is incredibly important. If you forget it the name property will be added to the global object instead of creating a new class. Many people have developed other tactics for avoiding this situation but they prevent inheritance from being as elegant as it can be so I do not recommend them. I have shown a common, and fairly decent one for reference.

function MobNewObject(name, health) {
 var r = {}; // Create a new object instead of this.
 r.name    = name;
 r._health = health || 100;
 return r; // Then return it.
}

This method works, and will not clobber the global object if you forget new, but if you do forget it you will not get any methods. This can again be worked around by using Object.create(MobNewObject.prototype) to ensure you get your prototype set but is just more work. Another problem with this technique is that it doesn't allow a caller to provide their own object if desired which makes inheritance fall apart.

Adding methods

Adding methods is as simple as creating new properties on the Mob.prototype object. You can simply assign functions and values to this object, but I prefer to use Object.defineProperty() so that I can make setters and getters, as well as control property writablilty, configurability and enumerability.

Object.defineProperties(Mob.prototype, {
 health: {
  get: function mob_health_get(){
   return this._health;
  },
  set: function mob_health_set(v){
   this._health = (v > 0) ? v : 0;
  },
  enumberable: true,
 },
 alive: {
  get: function mob_alive(){
   return this._health > 0;
  },
  enumberable: true,
 },
 damage: {
  value: function mob_damage(amt, who){
   this.health -= damage;
  },
  enumberable: true,
 },
 kill: {
  value: function mob_kill(){
   this._health = 0;
  },
  enumberable: true,
 },
});

Now you can instantiate and call the methods.

var pig = new Mob("Pig", 10);
pig.health += 5;
assert(pig.health === 15);
pig.damage(10, john);
assert(pig.health === 5);
pig.kill();
assert(pig.health === 0);
pig.health -= 10;
assert(pig.health === 0);
assert(pig.alive === false);

Inheritance

Now here comes the "tricky" part. It isn't really tricky but a lot of people get it wrong. Let's make a AggressiveMob type that attacks back.

function AggressiveMob(name, health, strength) {
 Mob.call(this, name, health);
 
 this.strength = strength;
}
AggressiveMob.prototype = Object.create(Mob.prototype, {
 constructor: { value: AggressiveMob },
 
 damage: {
  value: function aggressiveMob_damage(amt, who) {
   Mob.prototype.damage.apply(this, arguments); // Call super method.
   
   if (!this.alive) return; // Can't attack if dead.
   if (!(who instanceof Mob)) return; // Only damage Mobs.
   
   this.attack(who);
  },
  enumberable: true,
 },
 
 attack: {
  value: function aggressiveMob_attack(who) {
   who.damage(this.strength, this); 
  },
  enumberable: true,
 },
});

There we go. Now if an AggressiveMob is attacked it will fight back. And two AggressiveMobs will keep at it until one dies. Now let's look at how this works.

Construction

function AggressiveMob(name, health, strength) {
 Mob.call(this, name, health); // 1
// ...

Here we are creating the constructor for our subclass. This important bit (1) is calling the superclass's constructor. Note that we are not using new! This is because we want it to construct our object, rather than the new one that new creates.

This is why I don't recommend the other method mentioned. Instead of calling the constructor on this you can use the returned value as the "this". The problem with this method is setting the prototype manually (because you aren't trusting your users to use new). There is no standard way to set the prototype. There is an upcoming standard Object.setPrototypeOf() in ES6, but currently you can use the defacto standard .__proto__ property. This is what it looks like.

function AggressiveMob(name, health, strength) {
 var r = Mob(name, health);
 // Eventually we can use.
 //Object.setPrototypeOf(r, AggressiveMob.prototype);
 // But for now.
 r.__proto__ = AggressiveMob.prototype;
 
 // ...
 
 return r;
}

As I said before, it can work, but it is much less elegant. Just make your users use new.

Setting the prototype

Okay, back on track.

AggressiveMob.prototype = Object.create(Mob.prototype, { // 1
 constructor: { value: AggressiveMob }, // 2
// ...

The first line (1) sets the prototype of AggressiveMob to a new object which inherits from Mob.prototype. Some people accomplish this by assigning a new instance (AggressiveMob.prototype = new Mob(...)) but this also includes the instance variables, which is unnecessary and rarely may be undesirable. Another nice thing about Object.create() is that the second argument is an object like the second parameter to Object.defineProperties() which makes it easy to add methods.

The first property is how javascript designates what class an object is. It is rarely used but it is good practice to indicate it. Usually this property is set automatically but since we are replacing the prototype object in order to inherit from another class we have to re-assign it ourselves.

Calling Superclass Methods

 damage: {
  value: function aggressiveMob_damage(amt, who) {
   Mob.prototype.damage.apply(this, arguments); // 1

To call a superclass method you simply access it from the prototype object of the parent class. Then you call it on your object using apply or call. Notice that you don't have to access the superclass where a method was defined, always reference your direct parent and it will look up the prototype chain.

That's It! We're Done

That is really all there is to it. People always say that Javascript inheritance is difficult, but really it is just the confusing things that you see. People try to fight the stream and develop workarounds, but if you take a couple minutes to lean you will see that it is really quite simple, maybe a little tedious at times, but simple none the less.

No comments:

Post a Comment