Constructors, Prototypes, and Inheritance
21 Aug 2015Factories vs. Constructors
Factory
function makeRect(w,h) {
var rect = {};
rect.width = w;
rect.height= h;
rect.area = function() { //personal instance method
return this.width * this.height;
}
return rect;
}
var rect1 = makeRect(1,1);
rect1.area();
Freelance initializer method
function initRect(w,h) {
this.width = w;
this.height= h;
this.area = function() { //personal instance method
return this.height * this.width
}
}
var rect2 = {};
initRect.call(rect2,1,2);
rect2.area();
Constructors
A constructor ("Ctor") often replaces a factory. It's just a freelance initializer method intended to be called in a special way:
function Rect(w,h) {
this.width = w;
this.height = h;
this.area = function() {
return this.height * this.width
}
}
var rect3 = new Rect(3,1); // <-- operator 'new'
rect3.area();
A Ctor represents a "class" in JS, a family of objects (instances) constructed in a similar way and sharing a formal "membership" or "pedigree".
The new operator
Always used with constructors to create objects:
var obj = new Object(), // {}
arr = new Array(1,2,3),// [1,2,3]
fun = new Function('x','return x*2'),
num = new Number(7),
str = new String('boo');
EXERCISE: pseudo-new #1
Using this constructor:
function Rect(w,h) {
this.width = w || 1;
this.height = h || 1;
this.area = function() {
return this.height * this.width
}
}
var trueRect = new Rect();
Try emulating new as a function:
function new1(Ctor) {
//...
}
It should work like so:
var fakeRect = new1(Rect);
fakeRect.area(); // 1
But compare:
trueRect instanceof Rect //true
fakeRect instanceof Rect //false
trueRect.constructor //Rect
fakeRect.constructor //Object
Pseudo-instance (made with pseudo-new) has wrong pedigree!
Operator new
must be doing something more...
EXERCISE: pseudo-new #2
Using the same Rect
constructor as before, let's supplement our pseudo-new with one extra step:
function new2(Ctor) {
var inst = {};
inst.__proto__ = Ctor.prototype; // add "magic stamp"
Ctor.call(inst);
return inst;
}
var fakeRect = new2(Rect);
Now compare again:
trueRect instanceof Rect //true
fakeRect instanceof Rect //true
trueRect.constructor //Rect
fakeRect.constructor //Rect
Prototypes
Exercise 1: Basics
- First make a constructor named Ctor for an object that has properties a and b and initializes them to 0 and 1 respectively.
- Now, make two objects named obj1 and obj2 using Ctor.
- Now check the properties of a new object obj3 made this way:
var obj3 = {};
Ctor.call(obj3);
- Next, add a property c to obj1 with a value of 2. What will be the value of obj2.c?
- Now, add a property d with the value 3 to obj1's "proto" (the object which helps out when obj1 can't do something by itself). Remember that there are at least four ways of referring to that proto object.
- What are the values of obj1.d, obj2.d, and obj3.d? Can you explain the results?
Exercise 2: Deque Class
Write a constructor which constructs a special kind of object called a 'deque' (short for 'Double-Ended-QUEue'). A deque is like an array, but it can only be accessed from its two ends, like a roll of mints. Each deque instance should have a property holding an array and should have four methods:
deque.push(item)
deque.pop()
deque.unshift(item)
deque.shift()
Each deque's array will be a personal property installed by the constructor, but its methods should be shared by all deque instances. Attach the methods to Deque's prototype so that they will be inherited by the instances. Each method will use 'this' to refer to the deque instance, then make a change to that deque's array using the array method of the same name.
Usage is like so:
var deque = new Deque();
deque.push(2);
deque.unshift(1);
deque.push(3);
deque.shift(); //--> 1
deque.pop(); //--> 3
Exercise 3: Modifying Prototypes
Consider this code:
function A() {};
//set default values for instances of A:
A.prototype = {num:0, str:'default'};
var objA = new A();
function B() {};
// set default values for instances of B:
B.prototype.num = 0;
B.prototype.str = 'default';
var objB = new B();
There is a difference between the behaviors of objA
and objB
! Explain.
Exercise 4: 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() {
var area = function() {
return (this.width() * this.height());
}
function Ctor(w,h) {
this.width = w || 1;
this.height = h || 1;
this.area = area;
}
return Ctor;
})()
Modify the Rect module so that the instance method
area
is inherited from a prototype.In a new IIFE, implement a subclass of
Rect
calledSquare
. TheSquare
constructor needs only one parameter:Square(size)
, and it should call the parent class constructor (Rect(width,height)
) to set the new instance's properties. A Square instance should inherit thearea
method of Rectangles without needing any changes.Within the Square module, add an inheritable 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(1,1), r2 = new Rect(2,2), r3 = new Rect(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 new2(Ctor) { var instance = {}; instance.__proto__ = ctor.prototype; ctor.call(instance); //does initialization return instance; };
Simplify
fakeNew
by usingObject.create
.
Exercise 5: Imaginary Menagerie
a) Implement a simple taxonomy of four related classes, using a constructor for each:
- Animal: every instance of an Animal should inherit a method called move(). For basic animals, this just returns the string "walk". This method will be overridden by subclasses of Animal.
- Bird: A subclass of Animal. Every Bird instance should return "fly" instead of "walk" when asked to move(). All Birds also inherit a property hasWings which is true.
- Fish: Another subclass of Animal. A Fish instance will "swim" instead of "walk".
- Penguin: A subclass of Bird. Penguins cannot fly and they should move like Fish.
Every instance of Animal and its subclasses should also have a personal name property which is not inherited. It should be set only within the constructor Animal, and each subclass constructor should first call its superclass constructor as an initializer, all the way up to Animal.
You should see these behaviors:
new Animal("Simba").move();// 'walk'
new Fish("Nemo").move(); // 'swim'
new Bird("Lulu").move(); // 'fly'
var pengo = new Penguin("Pengo");
pengo.name; // 'Pengo'
pengo.move(); // 'swim'
pengo.hasWings; // true;
pengo instanceof Penguin; //true
pengo instanceof Bird; //true
pengo instanceof Animal; //true
b) Create a class Egg, whose instances have one method, hatch(name), which returns a new instance (named name) of the same species which laid the egg. Assume that every Animal can lay an egg with an instance method layEgg() which creates a new Egg instance. Try to solve this without subclassing Egg and without implementing layEgg and hatch more than once.
You should see this behavior:
var pengo = new Penguin("Pengo");
var egg = pengo.layEgg();
egg.constructor === Egg; //true
var baby = egg.hatch("Penglet");
baby instanceof Penguin; //true
var nemo = new Fish("Nemo");
egg = nemo.layEgg();
egg.constructor === Egg; //true
baby = egg.hatch("Nemolet");
baby instanceof Fish; //true