6

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?
taskinoor
  • 45,586
  • 12
  • 116
  • 142

4 Answers4

3

@@@ 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?

Yea. This example is a inheritance-learning style. For using in your application, try:

    function F() {}
    F.prototype = Base.prototype; // Linking to Base's prototype

    Derived.prototype = new F(); // The least memory-consumption object.
    Derived.prototype.constructor = Base; // Constructor reference correction

@@@ 1. What to do with data members in prototype object (_n in this example)?

Using the above prototype-chain, we are not creating any instance of Base. So, this question is invalidated.

3

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.)

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • With all due respect your solution is massively overkill. I'd recommend you take a look at [some of my OO articles](http://www.raynos.org/blog/5/Doing-Object-Oriented-Javascript---part-2) and [axel's prototypes as classes](http://www.2ality.com/2011/06/prototypes-as-classes.html). Your behind on how we do things with ES5. – Raynos Oct 30 '11 at 13:03
  • @Raynos: The above doesn't use ECMAScript5 because A) It was written before the standard was completed, and more importantly B) You can't use ECMAScript5 in web pages in the real world yet. About 3/4ths of my JavaScript is targeted for real-world use in web pages (the rest is server-side), thus... Also, 218 lines (heavily-commented; 2,115 characters minified) is **hardly** "massively overkill." – T.J. Crowder Oct 30 '11 at 13:09
  • Thanks for really nice and detailed explanation. – taskinoor Oct 30 '11 at 13:11
  • Do you have a good objection to using ES5-shim. Also the loc is not massively overkill. What's massively overkill is using a custom abstraction / class factory when you can use ES5 instead. Personally I would say there is never a need for a class factory nor is there a need for `super` sugar. – Raynos Oct 30 '11 at 13:12
  • @Raynos: "Do you have a good objection to using ES5-shim."* Well, it's another script, which if I don't need it is needless overhead. But more to the point, it doesn't provide what my `Helper` stuff provides: Efficient, easy-to-use supercalls. There are ways in which ES5 can improve `Helper` marginally in places (specifically the constructor dance can be replaced with `Object.create`), but it doesn't *replace* it. So I might start using es5-shim in my projects at some point, but I'd still want my `Helper` as well. – T.J. Crowder Oct 30 '11 at 13:44
  • I am accepting Raynos's answer, mainly because I have no objection with ECMAScript5. I am learning WebGL which already requires modern browsers. Still this answer provides a lot of helpful information. Thanks again for your effort. – taskinoor Oct 30 '11 at 13:51
  • @Raynos: That's using one-off objects, which is easy. What does it look like with constructor functions? – T.J. Crowder Oct 30 '11 at 14:03
  • @T.J.Crowder [constructors](https://gist.github.com/1325915#file_constructors.js). I hope that's what you meant. – Raynos Oct 30 '11 at 14:10
  • @Raynos: No, I meant constructor functions -- things you use with `new`. Also note that the point of my mechanism is *efficient*, easy-to-use supercalls, so calling `super` which in turn calls `Object.getPrototypeOf` (which is dodgy in the shim anyway) isn't ideal (efficiency), and repeating the base object name is out (because that's a maintenance hole). I achieve this with less than 2k of highly-efficient code. Overhead on hierarchy creation is trivial, there is nearly *no* over head on supercalls, and it's easy to read/use. IMHO, of course. :-) – T.J. Crowder Oct 30 '11 at 14:25
  • @T.J.Crowder I be damned, my `super` function is broken. Your right, you can't elegantly do `super` without repeating the base object name. You need some kind of [wrapper function](https://gist.github.com/1326003) to inject the super reference. – Raynos Oct 30 '11 at 15:02
  • @T.J.Crowder As an aside @IvoWetzel has a [super mechanism](https://github.com/BonsaiDen/Project-Iar/blob/master/Class.js#L29) that uses `eval` to compile `super.method(...)` into the `correct_function.apply(this, [...])`. I claim this runtime js -> js compilation is elegant. – Raynos Oct 30 '11 at 15:08
  • @Raynos: Two issues with Ivo's mechanism: 1. It relies on function decompilation (`Function#toString`), which has never been standardized (in fact, standardizing it was expressly rejected for ES5) and doesn't work on some mobile browsers, and 2. It rather naively assumes all occurrences of "Super" in the function text should be replaced, regardless of what they're used for (so `var niftySuperThing;` becomes `var niftythis.Super_2Thing;`, which (all due respect to Ivo) makes me shudder. The mechanism also pollutes the object instances (with all those `Super_n` properties). – T.J. Crowder Oct 30 '11 at 15:24
1

But I don't like this line:

Derived.prototype = new Base;

Then replace it with

Derived.prototype = Object.create(Base.prototype);

See Object.create simply returns a new object whose [[Prototype]] is the first parameter you give it.

It's basically saying Derived inherits from Base but don't call that damned constructor!

What to do with data members in prototype object (_n in this example)?

When your chaining prototypes don't call the constructor! I wrote an JS OO part 3 article about this.

It basically says that when you create objects you instantiate and initialize.

// instantiate
var o = Object.create(Base.prototype);
// o now inherits all of Bases methods because o.[[Prototype]] === Base.prototype
// o also inherits the constructor function (Base.prototype.constructor === Base)
// initialize
o.constructor(10);

Now of course new X does both. Here's an over view of what new does

var new = function (constructor, ...args) {
  var instance = Object.create(constructor.prototype);
  instance.constructor(...args);
  return instance;
}

As you can see you don't want new because you don't want that constructor to be called (You don't need to initialize Derived.prototype).

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?

This concern with Object.create is void. instantiating an object is cheap. It just generates a new thing object whose internal [[Prototype]] property is a pointer to the prototype you pass in.

The only thing that can be expensive is the constructor, and you don't call the constructor.

Minor Disclaimer:

Object.create is ES5 and some legacy browsers (mainly IE8) don't support. However there's this lovely thing called the ES5-shim that fixes these browsers and makes them behave like ES5.

Raynos
  • 166,823
  • 56
  • 351
  • 396
  • 1
    You should probably mention the minor detail that `Object.create` is an ECMAScript5 feature, and not in some current and most older JavaScript engines in the wild. – T.J. Crowder Oct 30 '11 at 12:58
  • @T.J.Crowder I have a habit of translating [tag:javascript] to ES5 and assuming everyone uses the ES5-shim. – Raynos Oct 30 '11 at 13:06
  • Thanks for pointing towards Object.create. I didn't think about this. – taskinoor Oct 30 '11 at 13:12
  • *"I have a habit of translating javascript to ES5 and assuming everyone uses the ES5-shim"* Bad habit. – T.J. Crowder Oct 30 '11 at 13:13
  • 1
    @T.J.Crowder Meh, people should be more specific. I gave a perfectly sensible javascript solution to his problem. Legacy platform support isn't part of the deal (but hey if you want it ES5-shim does it). – Raynos Oct 30 '11 at 13:19
  • @T.J.Crowder that Object.create is not standards compliant. Including an ES5 emulation that isn't correct is just silly. – Raynos Oct 30 '11 at 13:20
  • @Raynos: I've deleted that comment, didn't look closely at the implementation in that answer. But a *fully* ES5-compliant version of `Object.create` **can't** be created in ES3, can it? But my point was, saying "some" when you're talking a about [45% of real-world browsers on desks](http://marketshare.hitslink.com/browser-market-share.aspx?qprid=2&qpcustomd=0) is disingenuous, although you can add a sufficiently-good version of it quite easily. – T.J. Crowder Oct 30 '11 at 13:26
  • @Raynos: Do you really think I would just pick a number out of the air? My ~45% is: 29.91% IE8 + 8.6% IE6 + 6.22% Firefox 3.6 (which doesn't have `Object.create`). 29.91 + 8.6 + 6.22 = 44.73. NetApplications (the link I gave) is one of the most respected sources for general-use browser stats in the world. It's the one Microsoft chose for http://ie6countdown.com. Wikipedia is just not a valid counter-link. – T.J. Crowder Oct 30 '11 at 13:29
  • @Raynos: I only bothered to add in the top ones. Pick an authority, the point is it's a high number. – T.J. Crowder Oct 30 '11 at 13:36
  • @T.J.Crowder your right, browser support is a non-trivial issue we should be aware of. – Raynos Oct 30 '11 at 13:38
-1

JavaScript is quite different from other "Object Oriented" languages, try forgetting what you know about other languages first, and learn how JavaScript works.

A good introduction can be found at JavaScript: The World's Most Misunderstood Programming Language by Douglas Crockford, he also describes how inheritance works in JS here.

EDIT: Trying to actually answer your question: Question 1, i don't understand at all... sorry. Question 2: Classical inheritance in JavaScript is fairly ugly, and I've never found a need for it either. What Crockford calls "Parasitic Inheritance" (the second link) I think solve this issue. Here the "parent" object is called inside the scope of the constructor.

Stein G. Strindhaug
  • 5,077
  • 2
  • 28
  • 41
  • This doesn't answer his questions, and I would have said (in fact, did say :-) ) that he's already done quite a good job of forgetting what he knows about other languages. – T.J. Crowder Oct 30 '11 at 12:39
  • I'm not quite sure what you are asking for really, but you seem to be "thinking in Java" which may cause confusion. Inheritance (is-a) is often the solution in Java, in JavaScript composition (has-a) is often more appropriate. – Stein G. Strindhaug Oct 30 '11 at 12:44
  • `is-a` vs. `has-a` is a red herring; both languages allow both, and both have their places in both languages. I think it's perfectly clear what he's asking. – T.J. Crowder Oct 30 '11 at 12:49
  • @T.J. Crowder; The reason I suspect he's still "thinking in Java" (even though he've learned a lot about JavaScript) is that he's concerned about inheritance in the first place. I personally have never felt the need for inheritance in JavaScript at all. – Stein G. Strindhaug Oct 30 '11 at 12:50
  • 1
    The need for inheritance (or lack thereof) is not language-specific. You can write Java code without inheritance; you can write JavaScript code *with* inheritance. There are reasons for doing both. But all of that aside, again, this doesn't **answer** the question. It *comments* on the question saying, basically, "don't use inheritance in JavaScript". Comments should be *comments*, not answers. – T.J. Crowder Oct 30 '11 at 12:57
  • 1
    @SteinG.Strindhaug "never felt the need for inheritance in javascript" THEN GO LEARN JAVASCRIPT. seriously. The prototype is really wonderful and powerful. Don't tell someone else to learn javascript. – Raynos Oct 30 '11 at 13:01