Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JS: Confusion about inheritance

Tags:

javascript

I am familiar with OOP concepts through the languages like C++, Java. Right now I am trying to learn JavaScript as a hobby, mainly due to the interest in WebGL. But I am having troubles with prototype based inheritance.

Let's say I have a base class which accepts a parameter in constructor. And I need to extend this. The way I am doing this is shown below.

function Base(n) {
    this._n = n;
}

Base.prototype.print = function() {
    console.log(this._n);
}

function Derived(n) {
    Base.call(this, n);
}

Derived.prototype = new Base;
Derived.prototype.constructor = Derived;

Now this is what I understand: A single Base object is serving as the prototype of Derived. So all instances of Derived will inherit properties from this Base object, e.g. the print method. When I call new Derived(10) then a new object is created, function Derived is called in the context of this newly created object, i.e. this points to newly created object, and function Base is called from function Derived, and then _n is created and assigned the value 10. So if I create 5 Derived objects, all of them will have their own _n property. So far this is okay.

But I don't like this line:

Derived.prototype = new Base;

Function Base expects an argument but I am passing nothing here. There is no point of passing a parameter here as this object will act as prototype of Derived. And I don't need any value of _n for this prototype object. But what if the function Base depends on the argument? Say, Base loads a resource and the path is passed as parameter. What to do then?

To summarize, my questions are:

  1. What to do with data members in prototype object (_n in this example)?
  2. Derived.prototype = new Base; is creating an instance of Base and this will remain in memory always (assuming Derived is defined in global space). What to do if Base class is very costly and I don't want an extra object?
like image 436
taskinoor Avatar asked Oct 30 '11 12:10

taskinoor


People also ask

Why is inheritance bad in JavaScript?

Class inheritance by virtue of its mechanisms create class hierarchies as a side-effect of sub-class creation. Those hierarchies lead to arthritic code (hard to change) and brittleness (easy to break due to rippling side-effects when you modify base classes).

Does JavaScript support inheritance?

When it comes to inheritance, JavaScript only has one construct: objects. Each object has a private property which holds a link to another object called its prototype. That prototype object has a prototype of its own, and so on until an object is reached with null as its prototype.

How is inheritance different in JavaScript?

Classes inherit from classes and create subclass relationships: hierarchical class taxonomies. In JavaScript, class inheritance is implemented on top of prototypal inheritance, but that does not mean that it does the same thing: JavaScript's class inheritance uses the prototype chain to wire the child `Constructor.

Does JS support class like inheritance?

JavaScript supports inheritance if you use it. Look up "JavaScript prototype chain". As for "Why javascript does not support classical inheritance as a default option?" - because that's how JavaScript was defined.


1 Answers

First off, kudos on understanding JavaScript's prototypical inheritance so well. You've clearly done your homework. Most people coming from a Java or C++ background tend to really struggle, but you've gotten past the worst of it.

Function Base expects an argument but I am passing nothing here. What to do with data members in prototype object (_n in this example)?

If you need to use Base as a base, you need to design it to accept zero arguments reasonably, or you need to call it with arguments when creating the base object for Derived. Those are basically your only two options.

Derived.prototype = new Base; is creating an instance of Base and this will remain in memory always (assuming Derived is defined in global space). What to do if Base class is very costly and I don't want an extra object?

It's just the same as static data in Java classes: Loading the class loads that data. If you're going to use Base as a base, you'd want to design it so it doesn't load a bunch of stuff it doesn't need (perhaps by handling the zero-argument version differently than the with-argument version).

And it's that last approach (handling zero-argument construction differently than with-argument construction) that you usually see in "class" systems for JavaScript. Typically you'll see the actual constructor function used only to construct a raw object, and some other named function used to actually initialize instances (initialize is the name Prototype uses, and that I used when doing my replacement/revision of Prototype's mechanism). So the actual constructor function takes no arguments, but then you would initialize an instance by calling the initialize function (which in turn calls its base's initialize function). In most wrappers, that's handled for you under-the-covers.

Making that constructor-vs-initializer mechanism work in practice requires some tricky plumbing because it requires "supercalls" (calls to the base's version of a function), and supercalls are awkward in JavaScript. (That — supercalls — is actually what the linked article is mostly about, but exploring an efficient approach to them also involved creating/updating an entire inheritance system. I really need to update that article so it doesn't use class-based terminology; it's still prototypical, it just provides that plumbing I was talking about.)

Because external resources can disappear / get moved / etc. and Stack Overflow is meant to mostly stand alone, here's the end result of the iterations presented in the article linked above:

// Take IV: Explicitly handle mixins, provide a mixin for calling super when
// working with anonymous functions.
// Inspired by Prototype's Class class (http://prototypejs.org)
// Copyright (C) 2009-2010 by T.J. Crowder
// Licensed under the Creative Commons Attribution License 2.0 (UK)
// http://creativecommons.org/licenses/by/2.0/uk/
var Helper = (function(){
    var toStringProblematic,    // true if 'toString' may be missing from for..in
        valueOfProblematic;     // true if 'valueOf' may be missing from for..in

    // IE doesn't enumerate toString or valueOf; detect that (once) and
    // remember so makeClass can deal with it. We do this with an anonymous
    // function we don't keep a reference to to minimize what we keep
    // around when we're done.
    (function(){
        var name;

        toStringProblematic = valueOfProblematic = true;
        for (name in {toString: true, valueOf: true}) {
            if (name == 'toString') {
                toStringProblematic = false;
            }
            if (name == 'valueOf') {
                valueOfProblematic = false;
            }
        }
    })();

    // This function is used to create the prototype object for our generated
    // constructors if the class has a parent class. See makeConstructor for details.
    function protoCtor() { }

    // Build and return a constructor; we do this with a separate function
    // to minimize what the new constructor (a closure) closes over.
    function makeConstructor(base) {

        // Here's our basic constructor function (each class gets its own, a
        // new one of these is created every time makeConstructor is called).
        function ctor() {
            // Call the initialize method
            this.initialize.apply(this, arguments);
            }

        // If there's a base class, hook it up. We go indirectly through `protoCtor`
        // rather than simply doing "new base()" because calling `base` will call the base
        // class's `initialize` function, which we don't want to execute. We just want the
        // prototype.
        if (base) {
            protoCtor.prototype = base.prototype;
            ctor.prototype = new protoCtor();
            protoCtor.prototype = {};   // Don't leave a dangling reference
        }

        // Set the prototype's constructor property so `this.constructor` resolves
        // correctly
        ctor.prototype.constructor = ctor;

        // Flag up that this is a constructor (for mixin support)
        ctor._isConstructor = true;

        // Return the newly-constructed constructor
        return ctor;
    }

    // This function is used when a class doesn't have its own initialize
    // function; since it does nothing and can only appear on base classes,
    // all instances can share it.
    function defaultInitialize() {
    }

    // Get the names in a specification object, allowing for toString and
    // valueOf issues
    function getNames(members) {
        var names,      // The names of the properties in 'members'
            name,       // Each name
            nameIndex;  // Index into 'names'

        names = [];
        nameIndex = 0;
        for (name in members) {
            names[nameIndex++] = name;
        }
        if (toStringProblematic && typeof members.toString != 'undefined') {
            names[nameIndex++] = 'toString';
        }
        if (valueOfProblematic && typeof members.valueOf != 'undefined') {
            names[nameIndex++] = 'valueOf';
        }
        return names;
    }

    // makeClass: Our public "make a class" function.
    // Arguments:
    // - base: An optional constructor for the base class.
    // - ...:  One or more specification objects containing properties to
    //         put on our class as members; or functions that return
    //         specification objects. If a property is defined by more than one
    //         specification object, the last in the list wins.
    // Returns:
    //     A constructor function for instances of the class.
    //
    // Typical use will be just one specification object, but allow for more
    // in case the author is drawing members from multiple locations.
    function makeClass() {
        var base,       // Our base class (constructor function), if any
            argsIndex,  // Index of first unused argument in 'arguments'
            ctor,       // The constructor function we create and return
            members,    // Each members specification object
            names,      // The names of the properties in 'members'
            nameIndex,  // Index into 'names'
            name,       // Each name in 'names'
            value,      // The value for each name
            baseValue;  // The base class's value for the name

        // We use this index to keep track of the arguments we've consumed
        argsIndex = 0;

        // Do we have a base?
        if (typeof arguments[argsIndex] == 'function' &&
            arguments[argsIndex]._isConstructor) {
            // Yes
            base = arguments[argsIndex++];
        }

        // Get our constructor; this will hook up the base class's prototype
        // if there's a base class, and mark the new constructor as a constructor
        ctor = makeConstructor(base);

        // Assign the members from the specification object(s) to the prototype
        // Again, typically there's only spec object, but allow for more
        while (argsIndex < arguments.length) {
            // Get this specification object
            members = arguments[argsIndex++];
            if (typeof members == 'function') {
                members = members();
            }

            // Get all of its names
            names = getNames(members);

            // Copy the members
            for (nameIndex = names.length - 1; nameIndex >= 0; --nameIndex) {
                name = names[nameIndex];
                value = members[name];
                if (base && typeof value == 'function' && !value._isMixinFunction) {
                    baseValue = base.prototype[name];
                    if (typeof baseValue == 'function') {
                            value.$super = baseValue;
                    }
                }
                ctor.prototype[name] = value;
            }
        }

        // If there's no initialize function, provide one
        if (!('initialize' in ctor.prototype)) {
            // Note that this can only happen in base classes; in a derived
            // class, the check above will find the base class's version if the
            // subclass didn't define one.
            ctor.prototype.initialize = defaultInitialize;
        }

        // Return the constructor
        return ctor;
    }

    // makeMixin: Our public "make a mixin" function.
    // Arguments:
    // - ...:  One or more specification objects containing properties to
    //         put on our class as members; or functions that return
    //         specification objects. If a property is defined by more than one
    //         specification object, the last in the list wins.
    // Returns:
    //     A specification object containing all of the members, flagged as
    //     mixin members.
    function makeMixin() {
        var rv,         // Our return value
            argsIndex,  // Index of first unused argument in 'arguments'
            members,    // Each members specification object
            names,      // The names in each 'members'
            value;      // Each value as we copy it

        // Set up our return object
        rv = {};

        // Loop through the args (usually just one, but...)
        argsIndex = 0;
        while (argsIndex < arguments.length) {
            // Get this members specification object
            members = arguments[argsIndex++];
            if (typeof members == 'function') {
                members = members();
            }

            // Get its names
            names = getNames(members);

            // Copy its members, marking them as we go
            for (nameIndex = names.length - 1; nameIndex >= 0; --nameIndex) {
                name = names[nameIndex];
                value = members[name];
                if (typeof value == 'function') {
                    value._isMixinFunction = true;
                }
                rv[name] = value;
            }
        }

        // Return the consolidated, marked specification object
        return rv;
    }

    // Return our public members
    return {
        makeClass: makeClass,
        makeMixin: makeMixin
        };
})();

Usage:

var Parent = Helper.makeClass(function(){
    function hierarchy() {
        return "P";
    }
    return {hierarchy: hierarchy};
});
var Child = Helper.makeClass(Parent, function(){
    function hierarchy() {
        return hierarchy.$super.call(this) + " < C";
    }
    return {hierarchy: hierarchy};
});
var GrandChild = Helper.makeClass(Child, function(){
    function hierarchy() {
        return hierarchy.$super.call(this) + " < GC";
    }
    return {hierarchy: hierarchy};
});
var gc = new GrandChild();
alert(gc.hierarchy()); // Alerts "P < C < GC"

If you dn't like the funcname.$super.call(...) notation for supercalls, here's a mix-in that lets you use a shorter/clearer version instead (but at a runtime cost):

// Define our CallSuper mixin
Helper.CallSuperMixin = makeMixin(function() {
    function callSuper(ref) {
        var f,          // The function to call
            args,       // Arguments to pass it, if we have any
            len,        // Length of args to pass
            srcIndex,   // When copying, the index into 'arguments'
            destIndex,  // When copying args, the index into 'args'
            rv;         // Our return value

        // Get the function to call: If they pass in a function, it's the
        // subclass's version so look on $super; otherwise, they've passed
        // in 'arguments' and it's on arguments.callee.$super.
        f = typeof ref == 'function' ? ref.$super : ref.callee.$super;

        // Only proceed if we have 'f'
        if (f) {
            // If there are no args to pass on, use Function#call
            if (arguments.length == 1) {
                rv = f.call(this);
            } else {
                // We have args to pass on, build them up.
                // Note that doing this ourselves is more efficient on most
                // implementations than applying Array.prototype.slice to
                // 'arguments', even though it's built in; the call to it
                // is expensive (dramatically, on some platforms).
                len = arguments.length - 1;
                args = new Array(len);
                srcIndex = 1;
                destIndex = 0;
                while (destIndex < len) {
                    args[destIndex++] = arguments[srcIndex++];
                }

                // Use Function#apply
                rv = f.apply(this, args);
            }
        }

        // Done
        return rv;    // Will be undefined if there was no 'f' to call
    }

    return {callSuper: callSuper};
});

And again, I really need to update the terminology so it's not class-based. (And probably look at how ECMAScript5 lets us do things slightly differently, because it adds some useful stuff like direct control over prototypes.)

like image 95
T.J. Crowder Avatar answered Oct 23 '22 03:10

T.J. Crowder