Very basic introduction to JavaScript prototypes and inheritance

Mad ramblings

I recently looked for a basic tutorial on JS prototypes and inheritance for a friend. I was shocked to find that even 'basic' tutorials go deep into technical details not needed for general use, yet breeze over (or just skip) the fundamentals. Now, don't get me wrong: in-depth technical details are very important, but (IMO) they cause nothing but confusion when starting out with JS as your first language. So I decided to write my own tutorial.

This tutorial will focus on fundamentals. When you're ready, I recommend you look at more in-depth details such as on MDN and TutorialsTeacher. I can also recommend Google's JavaScript Style Guide.

Introduction

The purpose of JS prototypes is to provide a means of inheritance (also known as code reuse). Inheritance is a tool used to drastically reduce copy-paste of code. In other words, if you have two or more parts of the application that do similar things, you may use prototyping to write the code once and dynamically reuse that code many times.

There are various approaches to prototyping; I will demonstrate the methods I believe to be the cleanest, most used, and most generally applicable.

For our example we'll be making fruits. Because fruits are simple. (unless it's guava... guava is never simple.)

The problem

You want to create a mango (major component of the application). Maybe also an apple. Management may request a banana at a later stage, but you're unsure.

You could (shouldn't) do something like this:
var apple1 = {
  colour: 'red',
  weightKg: 0.179
};

var apple2 = {
  colour: 'red',
  weightKg: 0.183,
  age: 32,
  increaseAgeBy: function(days) {
    this.age += days;
  }
};

var mango = {
  colour: 'orange-yellow',
  weightKg: 0.151
};

apple2.increaseAgeBy(1);
console.log(apple2.age); // prints '1'
The above is perfectly fine for one-off objects, but not good if you require multiple renditions of this throughout your application.

The issue here is that you need to copy-paste the code every time you need it. This creates multiple problems:
On to better use of grammar.

The solution

Instead of referring to the core code as 'apple' or 'pear', we may instead refer to it as 'fruit' outright. The fruit object would define the common characteristics that all fruit have. We then create our apple, which inherits all basic fruit stuff from the fruit object.

Let's get right to it.
// First, we define fruit generally. We use a function to do this. De facto
// convention is to capitalise function names if we intend to use them for
// inheritance. Functions used in this way are also referred to as
// constructors, or base objects.
function Fruit() {
  // We use this to inform the JS engine that we're specifically storing
  // these variables inside whatever Fruit we end up making.
  this.colour = '';
  this.age = 0;
  this.weightKg = 0;
}

// All fruit have the capability to age, so we define this as part of the
// prototype. The 'prototype' property is a special internal that JavaScript
// uses for this purpose.
Fruit.prototype.increaseAgeBy = function(days) {
  this.age += days;
};

// This is where the magic happens. We can create the new fruit from our Fruit
// object.
var apple1 = new Fruit();
var apple2 = new Fruit();
var mango = new Fruit();

// Our apples and mangos have inherited Fruit functionality and properties,
// which we may now use.
console.log(apple1.age); // prints '0'
apple1.increaseAgeBy(7);
console.log(apple1.age); // prints '7'

console.log(mango.age); // prints '0'
mango.increaseAgeBy(1);
mango.increaseAgeBy(3);
console.log(apple1.age); // prints '4'
We can now use our Fruit object to manage all our fruit.
Technical note

In this case we refer to the apples and mangos as instances of Fruit.

Technical note #2

I use Fruit to refer to the Fruit function, and 'fruit' to just talk about fruit in a general sense. This style will be followed throughout the document.

Constructor parameters

Following from the above, we may now do something like this:
// Initialise our third apple.
var apple3 = new Fruit();
apple3.colour = 'red';
apple3.age = 3;
apple3.weightKg = 0.123;
There are however nicer ways of working with this, namely: construction parameters. Parameters are an optional alternative to making initialisation more concise.

Let's start by rewriting our Fruit function:
// Add the options parameter, which we can then use to set up our object:
function Fruit(options) {
  this.colour = options.colour;
  this.age = options.age;
  this.weightKg = options.weightKg;
}
We can now create our fruit like so:
// Initialise our third apple.
  var apple3 = new Fruit({
  colour: 'red',
  age: 5,
  weightKg: 0.1,
});

// The old code still works:
console.log(apple3.age); // prints '5'
apple3.increaseAgeBy(3);
console.log(apple3.age); // prints '8'

Accessors and validation

Instead of directly accessing properties it's generally a good idea to, loosely speaking, 'hide' variable properties. I put 'hide' in quotes because we technically still have full access to them, but general convention is to pretend such properties don't exist when using Fruit elsewhere. This is done to make the lives of other people using Fruit in their code a little easier because it allows those people to ignore the way it works internally while still using Fruit to its full potential.

We call these 'hidden' properties private properties.
Note

You, the creator of the code, can also be though of as 'other' people because in time people tend to forget how they wrote their own code. The idea is to make your life as well as the lives of others a little easier.

Once we've made each property private we'll write functions to read from and write to them. This has the added advantage that we may add things like validation (or other functionality) if we wanted to, as we'll see in a minute.

An example of why we may want validation is as follows. You may have noticed an issue above: nothing stops us accidentally storing bad values. I could do something like this:
apple3.age = 'three, I think?';  // <-- this should be a number..
apple3.colour = true;            // <-- and this should be a string.
We make properties private by putting an underscore in front of them. We can then write prototype functions to read/write them, and check for obvious problems while we're at it:
function Fruit(options) {
  // Make our internals private.
  this._colour = options.colour;
  this._age = options.age;
  this._weightKg = options.weightKg;
}
// With that done, let's write prototypes to access them.
// Returns fruit colour, or undefined if not set. Fruit.prototype.getColour = function() { return this._colour; };
// Because we're accessing properties via function calls, we can // use our set functions to validate user input before storing it. // This prevents users them from accidentally doing something silly.
Fruit.prototype.setColour = function(colour) { // Ensure the user is actually passing us a string: if (typeof colour !== 'string') { throw 'setColour requires a string.'; } else { this._colour = colour; } }; // Returns fruit age, or undefined if not set. Fruit.prototype.getAge = function() { return this._age; };
// Not only can we ensure we're being passed the right type of // variable, but we can also do some sanity checking, like making // sure our age is a positive number.
Fruit.prototype.setAge = function(age) { if (typeof age !== 'number' || age < 0) { // Prevent bad value. throw 'Error: setAge requires a number, and it needs to be positive.'; } else { // Set age as requested. this._age = age; } }; // Returns fruit weight in kilograms, or undefined if not set. Fruit.prototype.getWeightKg = function() { return this._weightKg; };
// We can also check if the values seem reasonable. For example, is // a 50kg weight normal, or are we possibly confusing grams with // kilograms?
Fruit.prototype.setWeightKg = function(weight) { if (typeof weight !== 'number' || weight < 0) { // Prevent bad value. throw 'Error: setWeightKg requires a number, and it needs to be positive.'; } else { // Set age as requested. this._weightKg = weight; } // Warn if weight seems weird: if (weight > 50) { console.warn('Fruit weight set to ' + weight + 'kg - is this correct?'); } };
Now that we're using functions to access our properties, we might use write our implementation as follows:
var apple1 = new Fruit({
  colour: 'red',
  age: 5,
  weightKg: 0.1
});

apple1.setAge(3);       // works as expected.
apple1.setAge('three'); // Uncaught Error: setAge requires a number, and it
                        // needs to be positive.
Only one more thing remains: validation in our constructor. We could still do something invalid like:
var apple1 = new Fruit({
  age: 'lol 3 apples.'
});
// ^^ no errors are thrown if we do this.
We can fix this by simply referencing our set functions from the constructor:
function Fruit(options) {
  // This insures we use our sanity checkers.
  this._colour = this.setColour(options.colour);
  this._age = this.setAge(options.age);
  this._weightKg = this.setWeightKg(options.weightKg);
}
Creating a fruit with invalid options will now fail:
var apple1 = new Fruit({
  age: 'two days'
});
// ^^ Uncaught Error: setAge requires a number, and it needs to be positive.
With the above section done, we're done with the basics. You now know enough to start using prototypes in your daily life.

However, if you feel like going just a little further and doing something magical (albeit a tad more advanced), I'd like to introduce you to:

Getters and setters

The above methods will work well, but things are now a little.. verbose. Wouldn't it be nice if instead of the above implementation, we could simply use our code this way:
var mango = new Fruit({
  age: 3
});

// Assign directly instead of using a function.
mango.age = 'orange';
// ^^ Uncaught Error: Fruit age should be a (positive) number.
As it turns out, you can do exactly that. You can achieve this by adding special functionality to properties. You do this via special functions called getters and setters.

Getter/setter syntax looks like this:
function Vegetable() {
  this._color = null;
  this._taste = 'bad';
}

Vegetable.prototype = {
  get name() {
    return this._name;
  },

  set name(value) {
    this._name = value;
  },

  get taste() {
    return this._taste;
  },

  set taste(value) {
    if (value !== 'bad') {
      throw 'Error: all vegetables taste bad.';
    }
  }
};
With the above, we can then use our object as follows:
var celery = new Vegetable();
celery.colour = 'green'; // This works fine.
celery.taste = 'good';
// ^^ Uncaught Error: all vegetables taste bad.
Notice how celery (obviously) cannot taste good, so our code throws an error if we try to tell it lies.

We can apply the same pattern to our fruit:
/**
 * Fruit object. Used to (sometimes) create sweet things.
 * @param {object} options - Optional.
 * @constructor
 */
function Fruit(options) {
  // Check if we've been given an options object. If not, create an empty one.
  // Doing so makes coding a tad easier as we need less error checking code.
  if (typeof options === 'undefined') {
    options = {};
  }

  // Next, we assign default values for our private variables:
  this._colour = 'unknown';
  this._age = 0;
  this._weightKg = 0;

  // Now, let's process the options object and apply any options the
  // user asked for. We do this by looping through it and assigning values to
  // our fruit instance along the way.
  for (var property in options) {
    if (options.hasOwnProperty(property)) {
      this[property] = options[property];
      // ^^ Example of what this looks like to the computer:
      //      this['color'] = options['colour'];
    }
  }
}
// Next we write our getters and setters. The contents of our // getter / setter functions look pretty much exactly like our // previous prototype functions.
Fruit.prototype = { /** * Returns fruit colour. * @returns {string} */ get colour() { return this._colour; }, /** * Set fruit colour. * @param {string} value - Any valid CSS colour name. */ set colour(value) { // Ensure the user is actually passing us a string or null: if (typeof value !== 'string' && value !== null) { throw 'Error: Fruit colour may only be null or a string.'; } else { this._colour = value; } }, /** * Return the fruit's age, in days. * @returns {number} */ get age() { return this._age; }, /** * Sets the fruit age. * @param {number} age - Fruit age, in days. */ set age(age) { if (typeof age !== 'number' || age < 0) { throw 'Error: Fruit age should be a (positive) number.'; } else { this._age = age; } }, /** * Gets the fruit weight, in kilograms * @returns {number} */ get weightKg() { return this._weightKg; }, /** * Sets the fruit weight. * @param {number} weight - Fruit weight, in kilograms. * @returns {number} */ set weightKg(weight) { if (typeof weight !== 'number' || weight < 0) { throw 'Error: Fruit weigh should be (positive) number.'; } else { this._weightKg = weight; } // Warn if weight seems weird: if (weight > 50) { console.warn('Fruit weight set to ' + weight + 'kg - is this correct?'); } } };
// Finally, we add any additional prototype functions we need.
/** * Increases the fruit's age. * @param {number} days - Amount of time to increase fruit age by, in days. */ Fruit.prototype.increaseAgeBy = function(days) { this.age += days; }; /** * Kills the fruit. Colour is set to brown to reflect this. */ Fruit.prototype.kill = function() { this.colour = 'brown'; };
Technical note

The /** [stuff] */ comments are called JSDoc. It's a method of documenting code that is both human readable and parsable by IDEs. Using JSDoc will improve code autocompletion on any decent IDE.

Our fruit class is now production ready. We may define our fruits as needed, fully features with error checking and the like:
// Test initial style:
var apple = new Fruit();
apple.colour = 'red';
apple.increaseAgeBy(9);
console.log(apple.age); // 9

// Test constructor arguments:
var mango = new Fruit({
  colour: 'orange-yellow',
  weightKg: 0.151
});
console.log(mango.weightKg); // 0.151
// Test setting an invalid age:
mango.age = 'orange';
// ^^ Uncaught Error: Fruit age should be a (positive) number.

// Test passing a bad value to the constructor:
var badFruit = new Fruit({ colour: true });
// ^^ Uncaught Error: Fruit colour may only be null or a string.
Our Fruit instance is now functional, easy to use, and has built-in error checking.

Well done on making it to the end of the tutorial! Many great success and happy coding to you.



- aggregate1166877