Cascading JavaScript Constructors to Extend Data

© 2011, Martin Rinehart

When one class extends another (sometimes called different names) instances of the extending class must provide all the instance data of the extended class, plus its own new instance properties.

Repetition—the Simple Way

One simple way to achieve this is to make the constructors repetitive:

function A( p1, p2 ) {
    this.a = p1;
    this.b = p2;
}

// B extends A
function B( p1, p2, p3, p4 ) {
    this.a = p1;
    this.b = p2;
    this.c = p3;
    this.d = p4;
}

While simple methods are often the best methods, this one is seldom practical. First, retyping (even cut/pasting) is at best a nuisance and it's error prone. Second, if there are a lot of data members of the extended class ("a lot" is subjective, but it's more than two) these problems are magnified. Third, if you decide to add or delete properties in the extended class, you have to make the same change(s) to the extending class.

If you are not convinced yet that this simple method isn't your best choice, consider the situation as a third class extends the second. Picture the ways you can make a mess of even a simple three-class hierarchy, such as this one:

function A( p1, p2 ) {
    this.a = p1;
    this.b = p2;
}

// B extends A
function B( p1, p2, p3, p4 ) {
    this.a = p1;
    this.b = p2;
    this.c = p3;
    this.d = p4;
}

// C extends B
function C( p1, p2, p3, p4, p5, p6 ) {
    this.a = p1;
    this.b = p2;
    this.c = p3;
    this.d = p4;
    this.e = p5;
    this.f = p6;
} // end: C()

Cascading Constructors

A better choice is to cascade the constructors, with each delegating to an init() method. This is a cascade:

function A( p1, p2 ) {
    A.init( this, p1, p2 );
}

 A.init = function ( inst, p1, p2 ) {
     inst.a = p1; // "inst" here, not "this"
     inst.b = p2;
  }

// B extends A
function B( p1, p2, p3, p4 ) {
    A.init( this, p1, p2 );
    B.init( this, p3, p4 );
}

 B.init = function ( inst, p3, p4 ) {
     inst.c = p3;
     inst.d = p4;
  }

// C extends B
function C( p1, p2, p3, p4, p5, p6 ) {
    A.init( this, p1, p2 );
    B.init( this, p3, p4 );
    C.init( this, p5, p6 );
}

 C.init = function ( inst, p5, p6 ) {
     inst.e = p5;
     inst.f = p6;
  }

The constructors now just call the init methods. Let's make one more improvement before we begin to add complications.

The init() methods themselves could call the init() methods of the classes they extend, this way:

function A( p1, p2 ) {
    A.init( this, p1, p2 );
}

A.init = function ( inst, p1, p2 ) {
    inst.a = p1; // "inst" here, not "this"
    inst.b = p2;
}

// B extends A
function B( p1, p2, p3, p4 ) {
    B.init( this, p1, p2, p3, p4 );
}

B.init = function ( inst, p1, p2, p3, p4 ) {
    A.init( inst, p1, p2 );
    inst.c = p3;
    inst.d = p4;
}

// C extends B
function C( p1, p2, p3, p4, p5, p6 ) {
    C.init( this, p1, p2, p3, p4, p5, p6 );
}

C.init = function ( inst,
        p1, p2, p3, p4, p5, p6 ) {
    B.init( inst, p1, p2, p3, p4 );
    inst.e = p5;
    inst.f = p6;
}

The three-class hierarchy here, with two methods for each class, may be disguising the fact that this is very, very simple.

The Simplicity of the Cascade

Let's look at the constructors and the init() methods in isolation. These are the constructors:

function A( p1, p2 ) {
    A.init( this, p1, p2 );
}

// B extends A
function B( p1, p2, p3, p4 ) {
    B.init( this, p1, p2, p3, p4 );
}

// C extends B
function C( p1, p2, p3, p4, p5, p6 ) {
    C.init( this, p1, p2, p3, p4, p5, p6 );
}

Each constructor does nothing more than pass its parameters on to its own init() method. Except that each class adds more parameters (which is the essential nature of classes that extend other classes) this could hardly be simpler. We'll get to the reason for "outsourcing" the constructor's work in the next section.

Before we get to the constructor-specific issues, lets look at the methods that do the initialization. Leaving the constructors aside, these are:

A.init = function ( inst, p1, p2 ) {
    inst.a = p1; // "inst" here, not "this"
    inst.b = p2;
}

B.init = function ( inst, p1, p2, p3, p4 ) {
    A.init( inst, p1, p2 );
    inst.c = p3;
    inst.d = p4;
}

C.init = function (
        inst, p1, p2, p3, p4, p5, p6 ) {
    B.init( inst, p1, p2, p3, p4 );
    inst.e = p5;
    inst.f = p6;
}

The base of the hierarchy simply handles its own parameters. After that, each extending class passes all but its own parameters on to the init() of the extended class and then handles its own. The extending classes don't care if the classes they extend may (or may not) be extending other classes. It's not their problem. (Jargon: encapsulation. Each class worries about its own problems and lets other classes take care of their own issues.)

If you haven't done so yet, now would be a good time to create your own code. You can copy mine slavishly, or you can invent your own and have some fun. Test it with some alerts, such as these:

var i0 = new A( 1, 2 );
alert( i0.a ); // 1

var i1 = new B( 3, 4, 5, 6 );
alert( i1.a + ', ' + i1.c ); // 3, 5

var i2 = new C( 7, 8, 9, 10, 11, 12 );
alert( i2.a + ', ' + i2.c +
        ', ' + i2.e ); // 7, 9, 11

The Magic new Operator

I love a good magic show! "Oh, wow!" and "How'd he do that?" and "That's impossible!" Yes, good stuff. But not in code.

JavaScript's new operator has its share of magic. Way too much magic for me. Let's begin with the constructor property.

When you write a = new A(); JavaScript creates a new object and assigns it to the this variable inside the constructor. The new object is given a single property named the constructor. The value of the constructor is the reference we are using (the A in new A()). That gives us an invisible first line of code in the constructor:

function A() {
    this.constructor = A; // invisible code
    /* code that you wrote, here */
}

You also get, almost, a second invisible line, like this:

function A() {
    this.constructor = A; // invisible code

     // BEWARE!
    this.prototype =
         this.constructor.prototype;

    /* code that you wrote, here */
}

For some reason that I definitely do not understand, ECMA's TC 39 (Technical Committee 39 is the author of the JavaScript standard, ECMA 262) chose to have this.prototype be an "implicit" property, one that is implied but not stated and which you cannot read or write in your own code. It is also an impossible property. If you assign an actual prototype property to your own object, JavaScript will ignore it.

But this is getting ahead of ourselves. Data properties ("instance variables" if you come from a class-based background) do not search the prototype chain. They are a part of each instance, as we are doing here in our constructor cascade. All we need to note is that it is a very good idea to step outside the "magic" of the new operator and write our own code where we will be in charge of our own (and our object's) destiny.

Now let's step into the gray area between instance variables and instance methods just long enough so that we can provide proper toString() methods to report on our object instances' data values.

Reporting on Instances with toString() Methods

One of the first things I discovered in writing Java was that a toString() method was almost mandatory if you wanted to debug your code. The object instance needed to report what type of thing it was (from which class it came) and what its key properties were (so that, if it had lots of siblings, they weren't all identical twins). I made it a habit to write the toString() as soon as I'd decided on the key properties, and I laid it out this way:

ClassName{prop0=value0,prop1=value1,...}

Note that this form treats whitespace as if it were too expensive to be wasted here. There's a good reason. As soon as a class starts using objects from other classes as the values of properties, this starts to look like:

Class1{prop0=Class2{propX=valueX,propY=valueY},
prop1=...}

When there was an embedded extended class (say, B extends A) I wrote this:

B{A{propA0=valueA0,propA1=valueA1},propB0=...}

I strongly recommend that you adopt this standard in its entirety and use it until your own situation lets you be sure that a change is actually an improvement. This standard has lots of years and lots of objects behind it.

Now let's go back to our example where C extends B and B extends A. I want this report from the final instance of C in our alert() methods above:

C{B{A{ A's props }, Bs other props }, Cs other props }

Creating this output requires complete control over all the functions in a class hierarchy. You want to call C's toString() method. That method wants to call the toString() of class B, the class that C extended. In turn, the toString() of class B wants to call the toString() of class A, the class that B extended. You need a general-purpose method for plucking the exact toString() you need from a hierarchy that has a toString() for every class. This would be considered an advanced technique in Java, C++ or any other class-based language. Happily, it's not hard to do in JavaScript.

Let's get started with a toString() method for A. This shows class A, complete with its toString() method:

function A( p1, p2 ) {
    A.init( this, p1, p2 );
}

A.init = function ( inst, p1, p2 ) {
    inst.a = p1; // "inst" here, not "this"
    inst.b = p2;
}

 A.prototype.toString = function () {
     return 'A{' +
         'a=' + this.a + 
         ',b=' + this.b +
     '}';
 } // end: A.toString()

You can give yours a quick test:

var a = new A( 0, 1 );
alert( a ); // A{a=0,b=1}

That leaves us with just one more topic: how do we incorporate class A's toString() into class B's toString(). The method I use depends on call(), perhaps one of the two most obscure methods in all of JavaScript.

JavaScript's call() and apply() methods are near-identical twins. "What's the difference between 'call()' and 'apply()'?" is a good question to ask if you are interviewing JavaScript applicants with an inflated sense of their own JavaScript savvy.

Aside. If you are ever asked about 'call()' and 'apply()', the correct interview answer is, "Both are very obscure methods. I try not to use them because I don't believe the person who will maintain the code I write has ever seen them." Follow that with, "But would you like me to answer the question?" and you probably have a job. If the interviewer asks about 'call()' and 'apply()', the question is, "Do you want the job?"

That said, I'd like a solution that doesn't depend on call(). If you find one, please let me know!

Let's begin call() by considering the problem it solves. You have an instance method, say foo() which you would normally call by writing my_instance.foo( args ). You want to use the method, but you don't have an instance, such as my_instance. Absent an instance, the this variable inside the method is an orphan. call() to the rescue.

The call() method is assigned by the Function() constructor to every function in your code. As its name suggests, it calls the function of which it is a property. It assigns a reference to the this as its first argument and whatever arguments you would otherwise supply in your code are the remaining arguments. (apply() is the same except that it passes your arguments as an array. In practice I've never used an apply().)

The call() method is the basic underlying method behind all the function calls in your code. JavaScript "compiles" your code into call() calls. For example, obj.method( args ) "compiles" to method.call( obj, args ). (That's simplified. More exactly, if Class.prototype is a reference to the prototype of obj, obj.method( args ) "compiles" to Class.prototype.method.call( obj, args ).) You could write all your code using nothing but the call() method of all your functions. It would be harder code to read, harder code to write, but it could be done. call() is fundamental.

Now, that's quite a lot of explanation for a simple tool, one that I hope you'll use very sparingly. The following shows how I use it in B's toString() to call A's toString().

B.prototype.toString = function () {
    return 'B{' +
        A.prototype.toString.call( this ) +
        ',c=' + this.c +
        ',d=' + this.d +
    '}';
} // end: B.toString()

The best I'll say for that code is that there is no magic, no hidden-up-your-sleeve execution context or "implied" references. It yields to careful examination if you understand the call() method. Let's examine it. Begin with everything except the line that calls A's toString(). It's all plain JavaScript. Define a toString() var that holds a function. Inside the function, return 'B{' +. That gets you just what it says, a string 'B{' and some more stuff. The added stuff, skipping a line, gets you the values of properties c and d. Now the remaining line.

A.prototype.toString is the full name of the reference we have assigned to A's toString() method. This is where JavaScript will look when it finds that each instance of A doesn't have its own toString() property. We are going to apply the built-in call(), which is a method of every function, to call this function. The first argument to call() is the value for the this variable, inside the function that is being call()ed. Now go back to the definition of A.prototype.toString() and think about what happens.

You are up to the one slightly strange thing going on. You wrote A.prototype.toString to be called by instances of the A class. Or at least you thought you did. Now you are calling it with an instance of the B class. Hmmm.

What can you use to call a function? Actually, it doesn't really matter what the class of the object is. The important thing is that the object has all the properties used inside the function. If the function says this.xxx there has to be an xxx property of the this object. Now think about the way our constructor cascade works.

Every instance of class B is built by, among other things, calling A.init(), a method that assigns properties. In fact, it assigns all the properties that A.prototype.toString() reports on. Therefore, an instance of class B is also an instance of class A so it can be used by A.toString(). It has all the properties that A.toString() needs. It works.

Test time! On your own, write C's toString() method. Here's a really big, fat hint. Copy from B.toString(). Change exactly one or two letters, never more, on each line and you're done.

With constructor cascades and our slightly advanced method of handling nested calls to toString() methods of extended classes, all you need is a way to handle other methods of extended classes and you have completely mastered this business of extending classes in JavaScript. Handling methods is the last topic in the series:

Extending Instance Methods in JavaScript.


Feedback: MartinRinehart at gmail dot com

# # #