JS: Confusion about inheritance

后端 未结 4 543
轮回少年
轮回少年 2020-12-17 02:32

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 hav

相关标签:
4条回答
  • 2020-12-17 02:52

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

    0 讨论(0)
  • 2020-12-17 02:58

    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.

    0 讨论(0)
  • 2020-12-17 03:02

    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.

    0 讨论(0)
  • 2020-12-17 03:06

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

    0 讨论(0)
提交回复
热议问题