PCS JS Immersion

Constructors, Prototypes, and Inheritance

Factories 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;
})()
  1. Modify the Rect module so that the instance method area is inherited from a prototype.

  2. In a new IIFE, implement a subclass of Rect called Square. The Square 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 the area method of Rectangles without needing any changes.

  3. 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, and square.size(num) should set the size to num.

  4. 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 method every to constructor Rect 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
    

  5. Now reactivate your Square module and then re-run the sequence above. What is the value of all[0]===r1? What happened?

  6. Notice that constructor Square does not inherit the class method from its parent class Rect! Implement the class method every for Square as well, so that Square.every() will return all squares ever built.

Object.create

  1. Modify constructor Square to use Object.create instead of new Rect when making Square's prototype. Does that fix the problem in #5 above?

  2. 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 using Object.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