classes
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:
- It will have a
toString
method which will indetify its class name and specs. - It keep counting of how many objets made and give the count when asked.
- It will have a
color
property - It will have a
purpose
property - It will have a
draw
method. - 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:
- will have a width and height property
- will have a method to calculate the area
- will have a new toString method which will use old toString, to build a better toString
- 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”
- r1 -> Reactangle.prototype
- Reactangle.prototype -> Shape.prototype
- Reactangle -> Shape
- 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