Background

JavaScript has prototype based inheritence. ES6 introduces new class syntax, but this does not make the JavaScript an object oriented interitence language. The class syntax is syntactical sugar and the language still has prototype based inheritence.

Previously, I have written about the prototype chain. Understanding the prototype chain is the first step if one is to understand, how classes and inheritence work in Javascript, even if its with ES6 sugar.

Example

Lets build a class called Shape and then build a Square and Rectange. Our class will do a few things:

  1. It will have a toString method which will indetify its class name and specs.
  2. It keep counting of how many objets made and give the count when asked.
  3. It will have a color property
  4. It will have a purpose property
  5. It will have a draw method.
  6. It will have a unique id

Using the Function constructor

// shape.js

function Shape(purpose){
  Shape._count++;
  this.id = parseInt(Math.random() * 100000000);
  this.purpose = purpose;
}

Shape.prototype.toString = function() {
  return `SHAPE - ${this.id} - ${this.purpose}`
}

Object.defineProperty(Shape, "count", {
  get: function() { return this._count ? this._count : 0 }
});

Object.defineProperty(Shape.prototype, "color", {
  get: function() { return this._color},
  set: function(x) { this._color = x; }
});

Shape.prototype.draw = function(canvas){
  console.log("I am drawing")
}

module.exports = Shape;

// somefile.js
var Shape = require("js/helpers/shape.js")

console.log(`Number of Shapes Created: ${Shape.count}`)

var s1 = new Shape("for first test", "green");
console.log("s1 is: ", s1);
s1.draw({});

var s2 = new Shape("for second test", "yellow");
console.log("s1 is: ", s2);
s2.draw({});

console.log(`Number of Shapes Created: ${Shape.count}`)

// ["constructor", "toString", "color", "purpose", "draw"]
console.log(Object.getOwnPropertyNames(Shape.prototype));

// ["id", "_purpose", "_color"]
console.log(Object.getOwnPropertyNames(s1));

// ["id", "_purpose", "_color"]
console.log(Object.getOwnPropertyNames(s2));

In the above example all methods are on the prototype object of the Shape constructor function. The instances created have a reference to the constructor functions prototype object.

Using class keyword

class Shape {

  constructor(purpose, color) {
    Shape._count = Shape._count ? ++Shape._count : 1;

    this.id = parseInt(Math.random() * 100000000);
    this._purpose = purpose;
    this._color = color;
    console.log(`created a shape. count now is: ${Shape._count}`)
  };

  toString(){
    return `SHAPE - ${this.id} - ${this.purpose}`;
  };

  draw(canvas){
    console.log(`I am drawing ${this}. My Color is: ${this.color} and I am drawing on ${canvas}`);
  };

  // gets defined on the `Shape` object
  static get count(){
    return this._count ? this._count : 0
  };

  // gets defined on the `Shape.prototype` object
  get color(){
    return this._color;
  };

  set color(x){
    return this._color = x;
  };

  get purpose(){
    return this._purpose;
  };

  set purpose(x){
    return this._purpose = x;
  };

}

module.exports = Shape;

The above created Shape2 via the class constructor works exactly like the earlier Shape created via the function constuctor.

var Shape = require("js/helpers/shape2.js")

console.log(`Number of Shapes Created: ${Shape.count}`)

var s1 = new Shape("for first test", "green");
console.log("s1 is: ", s1);
s1.draw({});

var s2 = new Shape("for second test", "yellow");
console.log("s1 is: ", s2);
s2.draw({});

console.log(`Number of Shapes Created: ${Shape.count}`)

// ["constructor", "toString", "color", "purpose", "draw"]
console.log(Object.getOwnPropertyNames(Shape.prototype));
var arr = Object.getOwnPropertyNames(Shape.prototype)
console.assert(arr.includes("constructor"));
console.assert(arr.includes("toString"));
console.assert(arr.includes("color"));
console.assert(arr.includes("purpose"));
console.assert(arr.includes("draw"));

// ["id", "_purpose", "_color"]
console.log(Object.getOwnPropertyNames(s1));
arr = Object.getOwnPropertyNames(s1)
console.assert(arr.includes("id"));
console.assert(arr.includes("_purpose"));
console.assert(arr.includes("_color"));

// ["id", "_purpose", "_color"]
console.log(Object.getOwnPropertyNames(s2));
arr = Object.getOwnPropertyNames(s2)
console.assert(arr.includes("id"));
console.assert(arr.includes("_purpose"));
console.assert(arr.includes("_color"));

The static keyword puts the thing on the class object, in this case the Shape. No methods are on the instance. All methods are defined on the protoype object. Mehods with static get put on the class object.

Example Two

Lets expand on our previous example. Lets build a class called Rectangle. It will be built upon the shape class we built earlier. It will:

  1. will have a width and height property
  2. will have a method to calculate the area
  3. will have a new toString method which will use old toString, to build a better toString
  4. will have a new draw updated method
\\ rectangle2.js
var Shape = require("js/helpers/shape2.js");

class Rectangle extends Shape{

  constructor(purpose, color, width, height){
    super(purpose, color);
    this.width = width;
    this.height = height;
  };

  get width(){
    return this._width;
  };

  set width(x){
    this._width = x;
  };

  get height(){
    return this._height;
  };

  set height(x){
    this._height = x;
  };

  area(){
    return this.width * this.height;
  };

  toString(){
    return `Original toString: ${super.toString()}. Now we have added: width is ${this.width} & height is ${this.height}`
  };

  draw(){
    console.log(`drawing a rectangle with id ${this.id} with an area of ${this.area()} whose purpose is "${this.purpose}"`)
  };
}
var r1 = new Rectangle("good purpose", "black", 2, 4);
console.log(`i have created r1: ${r1}`, r1);
r1.draw();

console.log("number of rectangles created: ", Rectangle.count);

// ["id", "_purpose", "_color", "_width", "_height"]
// it also properties which were created in Shape
console.log(Object.getOwnPropertyNames(r1));

console.log(Object.getOwnPropertyNames(Rectangle.prototype));

console.assert(r1.__proto__ == Rectangle.prototype);
console.assert(Rectangle.prototype.__proto__ == Shape.prototype);
console.assert(Rectangle.__proto__ == Shape);

The above Rectangle class has been built with the new class and extends keyword. This make it a lot easier to setup the prototype relationships of inheritence.

Here we have the following relations setup. Read (->) as “has a reference to”

  1. r1 -> Reactangle.prototype
  2. Reactangle.prototype -> Shape.prototype
  3. Reactangle -> Shape
  4. r1 has properties which were in an instance of Shape

Methods on the instance

So far all our methods were on the prototype chain. This prevented rebuilding of them on each instance as all instances shared them. The methods on the prototype chain had reference to this and this resolved to the object on which it was called. In case of subclas, even when the method was on the prototype of a much superior class, this always refered to the class on which it was called. This is because of the scope-by-flow resolution of this. It dictates that when a method is called like obj followed by a . (dot) followed by a .methodName, then the this in the method points to the object before the dot.

However, we can define a method on the instance, this will cause it to be created on each instance. The advantage of this is that it will have access to variables defined inside that function/constructor event after the function has ended

Example

Lets re-write a part of the earlier Shape class so that the id is private to the class and can not be modified.

class Shape {

  constructor(purpose, color){
    var _id = parseInt(Math.random() * 100000000);

    this._purpose = purpose;
    this._color = color;

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

    Object.defineProperty(this, "id", {
      get: function() { return _id; }

    });
  };

}

Above inside the constructor we have a variable _id. The outside world does not have access to it, but the getId() and the getter of id property both do, thanks to functional scope. The outside world, can not change the value of id on a Shape but they can definitely see it.

var s1 = new Shape("x", "y");
console.log(s1.getId());
console.log(s1.id);
try { s1.id = 0; } catch(e) { console.log(e); }
console.log(s1.id);

new.target

Sometimes a developer may forget to call the constructor with the new keyword. This can have a variety of effects on the code and is potential for sneaky bug. I tend to use new.target to detect if the function was called with the new keyword as it will store a reference to the constructor function. When new is not used, it will have value of undefined

var Shape = function(...args){ // using rest param. all params are converted in to an array
  if(new.target == undefined){ // when `new` is missed, we will get `undefined`
    return new Shape(...args) // using spread operator. an array is convereted in to multiple params
  }
  this.x = args[0];
  this.y = args[1];
}

var s1 = new Shape(1,2);
console.assert(s1.x == 1);
console.assert(s1.y == 2);

var s2 = Shape(2,3);
console.assert(s2.x == 2);
console.assert(s2.y == 3);

References:

  • http://thecodeship.com/web-development/methods-within-constructor-vs-prototype-in-javascript/
  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super
  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
  • https://hacks.mozilla.org/2015/08/es6-in-depth-subclassing/
  • https://hacks.mozilla.org/2015/07/es6-in-depth-classes/
  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters
  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator