CoreJS: Inheritance and Subclassing
19 Aug 2015Review: Constructors and Prototypes
With your team, describe the three roles objects can play in a Javascript "class" and summarize the relationship between each pair:
Constructor vs. Instance
Constructor vs. Prototype
Prototype vs. Instance
Built-in Subclasses
Use the expressions below to infer the inheritance relationships between various Javascript classes:
Object instanceof Object
Object instanceof Function
Array instanceof Array
[] instanceof Array
[] instanceof Object
{} instanceof Object
{} instanceof Function
null instanceof Object
Function instanceof Function
Function instanceof Object
new Number instanceof Object
new String instanceof String
new String instanceof Object
Formulate a hypothesis about how operator instanceof
works.
Implementing Inheritance
Here is a module (IIFE) which provides a constructor Rect
which builds rectangle instances. The instance methods are shared but linked directly to each instance.
var Rect = (function() {
function Ctor(l,b,r,t) {
this.l = l;
this.b = b;
this.r = r;
this.t = t;
this.width = width;
this.height= height;
this.area = area;
this.move = move;
}
var width = function() {
return this.r - this.l;
}
var height = function() {
return this.t - this.b;
}
var area = function() {
return (this.width() * this.height());
}
var move = function(dx,dy) {
this.l += dx;
this.r += dx;
this.b += dy;
this.t += dy;
}
return Ctor;
})()
Modify the Rect module so that all instance methods are inherited from a prototype.
In a new IIFE, implement a subclass of
Rect
calledSquare
. TheSquare
constructor needs only three parameters:Square(left,bottom,size)
, and it should call the parent class constructor (Rect(left,bottom,right,top)
) to set the new instance's properties. A Square instance should inherit thewidth
,height
,area
, andmove
methods of Rectangles without needing any changes.Within the Square module, add an instance method
size
which acts as both a getter and setter for a square's size. That is,square.size()
should return the current size of square, andsquare.size(num)
should set the size to num.For the moment, disable your Square module (by commenting it out or disabling the call operator () which triggers the IIFE). Now Modify the Rect module so that the
Rect
constructor maintains a list of every instance it ever creates. Attach a class methodevery
to constructorRect
which returns that list.When finished, you should be able run the following sequence:
var r1 = new Rect(0,0,1,1), r2 = new Rect(0,0,2,2), r3 = new Rect(0,0,3,3), all = Rect.every(); //list of r1,r2,r3 all[0] === r1; //true
Now reactivate your Square module and then re-run the sequence above. What is the value of
all[0]===r1
? What happened?Notice that constructor
Square
does not inherit the class method from its parent classRect
! Implement the class methodevery
forSquare
as well, so thatSquare.every()
will return all squares ever built.
Object.create
Modify constructor
Square
to useObject.create
instead ofnew Rect
when making Square's prototype. Does that fix the problem in #5 above?Return to your earlier simulation of the new operator, which we approximated like this:
function fakeNew(ctor,arg) { var instance = {}; instance.__proto__ = ctor.prototype; ctor.call(instance,arg); //does initialization return instance; };
Simplify
fakeNew
by usingObject.create
.
Overriding Inheritance
Consider the module below which implements a simplified Deque class:
var Deque = (function () {
function Deque (vals) {
// unprotected version
this.array = vals.slice();
}
Deque.prototype.top = function () {
if (this.array.length)
return this.array[this.array.length-1];
}
Deque.prototype.bottom = function () {
if (this.array.length)
return this.array[0];
}
Deque.prototype.push = function(val) {
return this.array.push(val);
}
Deque.prototype.pop = function() {
return this.array.pop();
}
Deque.prototype.unshift = function(val) {
return this.array.unshift(val);
}
Deque.prototype.shift = function() {
return this.array.shift();
}
return Deque;
})();
In a new IIFE, implement a subclass of
Deque
calledStack
. A Stack is a kind of Deque which is top-access only, affording Last-In-First-Out (LIFO) storage. A Stack instance will inherit all the method of Deque, but you'll need to disable the three methods which allow access to the bottom of the Stack.In a similar way, implement another subclass of
Deque
calledQueue
. A Queue is a kind of Deque which affords First-In-First-Out storage, where items are pushed onto the top and shifted from the bottom. Only the bottom of a queue is visible.
Automated Inheritance through extend
The process of generating a subclass can be automated by giving every function a method to extend it with a subclass. We'll use a variant of that pattern later with Backbone, but here is a rough approximation of how it works:
Function.prototype.extend = function(protoProps) { // method of any function...
var Super = this; //the function to be subclassed
function Ctor() { // the subclass ctor
Super.call(this);
}
// Make Ctor a subclass of Super:
var proto = Object.create(Super.prototype); //the subclass prototype
Ctor.prototype = proto;
proto.constructor = Ctor;
// Copy protoProps into subclass prototype:
//_.extend(proto,protoProps);
// OR
for (var prop in protoProps) {
proto[prop] = protoProps[prop];
}
return Ctor;
}
Here is an example of it in use:
function Duck() {}
Duck.prototype.feet = 2;
Duck.prototype.noise = 'quack';
var MutantDuck = Duck.extend({feet:3});
var duck = new MutantDuck();
duck instanceof MutantDuck; //true
duck instanceof Duck; //true
// Inherited from MutantDuck:
duck.feet; // 3
duck.hasOwnProperty('feet'); //false
// Inherited from Duck:
duck.noise; // 'quack'
duck.hasOwnProperty('noise'); //false