Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
245 views
in Technique[技术] by (71.8m points)

javascript - JS: Confusion about inheritance

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?
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...