Emulate super in javascript

后端 未结 11 1106
生来不讨喜
生来不讨喜 2020-12-08 10:18

Basically is there a good elegant mechanism to emulate super with syntax that is as simple as one of the following

  • this.$super.prop()
相关标签:
11条回答
  • 2020-12-08 11:05

    Have a look at the Classy library; it provides classes and inheritance and access to an overridden method using this.$super

    0 讨论(0)
  • 2020-12-08 11:07

    In the spirit of completeness (also thank you everyone for this thread it has been an excellent point of reference!) I wanted to toss in this implementation.

    If we are admitting that there is no good way of meeting all of the above criteria, then I think this is a valiant effort by the Salsify team (I just found it) found here. This is the only implementation I've seen that avoids the recursion problem but also lets .super be a reference to the correct prototype, without pre-compilation.

    So instead of breaking criteria 1, we break 5.

    the technique hinges on using Function.caller (not es5 compliant, though it is extensively supported in browsers and es6 removes future need), but it gives really elegant solution to all the other issues (I think). .caller lets us get the method reference which lets us locate where we are in the prototype chain, and uses a getter to return the correct prototype. Its not perfect but it is widely different solution than what I've seen in this space

    var Base = function() {};
    
    Base.extend = function(props) {
      var parent = this, Subclass = function(){ parent.apply(this, arguments) };
    
        Subclass.prototype = Object.create(parent.prototype);
    
        for(var k in props) {
            if( props.hasOwnProperty(k) ){
                Subclass.prototype[k] = props[k]
                if(typeof props[k] === 'function')
                    Subclass.prototype[k]._name = k
            }
        }
    
        for(var k in parent) 
            if( parent.hasOwnProperty(k)) Subclass[k] = parent[k]        
    
        Subclass.prototype.constructor = Subclass
        return Subclass;
    };
    
    Object.defineProperty(Base.prototype, "super", {
      get: function get() {
        var impl = get.caller,
            name = impl._name,
            foundImpl = this[name] === impl,
            proto = this;
    
        while (proto = Object.getPrototypeOf(proto)) {
          if (!proto[name]) break;
          else if (proto[name] === impl) foundImpl = true;
          else if (foundImpl)            return proto;
        }
    
        if (!foundImpl) throw "`super` may not be called outside a method implementation";
      }
    });
    
    var Parent = Base.extend({
      greet: function(x) {
        return x + " 2";
      }
    })
    
    var Child = Parent.extend({
      greet: function(x) {
        return this.super.greet.call(this, x + " 1" );
      }
    });
    
    var c = new Child
    c.greet('start ') // => 'start 1 2'
    

    you can also adjust this to return the correct method (as in the original post) or you can remove the need to annotate each method with the name, by passing in the name to a super function (instead of using a getter)

    here is a working fiddle demonstrating the technique: jsfiddle

    0 讨论(0)
  • 2020-12-08 11:09

    Here's my version: lowclass

    And here's the super spaghetti soup example from the test.js file (EDIT: made into running example):

    var SomeClass = Class((public, protected, private) => ({
    
        // default access is public, like C++ structs
        publicMethod() {
            console.log('base class publicMethod')
            protected(this).protectedMethod()
        },
    
        checkPrivateProp() {
            console.assert( private(this).lorem === 'foo' )
        },
    
        protected: {
            protectedMethod() {
                console.log('base class protectedMethod:', private(this).lorem)
                private(this).lorem = 'foo'
            },
        },
    
        private: {
            lorem: 'blah',
        },
    }))
    
    var SubClass = SomeClass.subclass((public, protected, private, _super) => ({
    
        publicMethod() {
            _super(this).publicMethod()
            console.log('extended a public method')
            private(this).lorem = 'baaaaz'
            this.checkPrivateProp()
        },
    
        checkPrivateProp() {
            _super(this).checkPrivateProp()
            console.assert( private(this).lorem === 'baaaaz' )
        },
    
        protected: {
    
            protectedMethod() {
                _super(this).protectedMethod()
                console.log('extended a protected method')
            },
    
        },
    
        private: {
            lorem: 'bar',
        },
    }))
    
    var GrandChildClass = SubClass.subclass((public, protected, private, _super) => ({
    
        test() {
            private(this).begin()
        },
    
        reallyBegin() {
            protected(this).reallyReallyBegin()
        },
    
        protected: {
            reallyReallyBegin() {
                _super(public(this)).publicMethod()
            },
        },
    
        private: {
            begin() {
                public(this).reallyBegin()
            },
        },
    }))
    
    var o = new GrandChildClass
    o.test()
    
    console.assert( typeof o.test === 'function' )
    console.assert( o.reallyReallyBegin === undefined )
    console.assert( o.begin === undefined )
    <script> var module = { exports: {} } </script>
    <script src="https://unpkg.com/lowclass@3.1.0/index.js"></script>
    <script> var Class = module.exports // get the export </script>

    Trying invalid member access or invalid use of _super will throw an error.

    About the requirements:

    1. this.$super must be a reference to the prototype. i.e. if I change the super prototype at run-time this change will be reflected. This basically means it the parent has a new property then this should be shown at run-time on all children through super just like a hard coded reference to the parent would reflect changes

      No, the _super helper doesn't return the prototype, only an object with copied descriptors to avoid modification of the protected and private prototypes. Furthermore, the prototype from which the descriptors are copied from is held in the scope of the Class/subclass call. It would be neat to have this. FWIW, native classes behave the same.

    2. this.$super.f.apply(this, arguments); must work for recursive calls. For any chained set of inheritance where multiple super calls are made as you go up the inheritance chain, you must not hit the recursive problem.

      yep, no problem.

    3. You must not hardcode references to super objects in your children. I.e. Base.prototype.f.apply(this, arguments); defeats the point.

      yep

    4. You must not use a X to JavaScript compiler or JavaScript preprocessor.

      yep, all runtime

    5. Must be ES5 compliant

      Yes, it includes a Babel-based build step (f.e. lowclass uses WeakMap, which is compiled to a non-leaky ES5 form). I don't think this defeats requirement 4, it just allows me to write ES6+ but it should still work in ES5. Admittedly I haven't done much testing of this in ES5, but if you'd like to try it, we can definitely iron out any builds issues on my end, and from your end you should be able to consume it without any build steps.

    The only requirement not met is 1. It would be nice. But maybe it is bad practice to be swapping out prototypes. But actually, I do have uses where I would like to swap out prototypes in order to achieve meta stuff. 'Twould be nice to have this feature with native super (which is static :( ), let alone in this implementation.

    To double check requirement 2, I added the basic recursive test to my test.js, which works (EDIT: made into running example):

    const A = Class((public, protected, private) => ({
        foo: function (n) { return n }
    }))
    
    const B = A.subclass((public, protected, private, _super) => ({
        foo: function (n) {
            if (n > 100) return -1;
            return _super(this).foo(n+1);
        }
    }))
    
    const C = B.subclass((public, protected, private, _super) => ({
        foo: function (n) {
            return _super(this).foo(n+2);
        }
    }))
    
    var c = new C();
    console.log( c.foo(0) === 3 )
    <script> var module = { exports: {} } </script>
    <script src="https://unpkg.com/lowclass@3.1.0/index.js"></script>
    <script> var Class = module.exports // get the export </script>

    (the class header is a bit long for these little classes. I have a couple ideas to make it possible to reduce that if not all the helpers are needed up front)

    0 讨论(0)
  • 2020-12-08 11:10

    I came up with a way that will allow you to use a pseudo keyword Super by changing the execution context (A way I have yet to see be presented on here.) The drawback that I found that I'm not happy with at all is that it cannot add the "Super" variable to the method's execution context, but instead replaces it the entire execution context, this means that any private methods defined with the method become unavailable...

    This method is very similar to the "eval hack" OP presented however it doesn't do any processing on the function's source string, just redeclares the function using eval in the current execution context. Making it a bit better as both of the methods have the same aforementioned drawback.

    Very simple method:

    function extend(child, parent){
    
        var superify = function(/* Super */){
            // Make MakeClass scope unavailable.
            var child = undefined,
                parent = undefined,
                superify = null,
                parentSuper = undefined,
                oldProto = undefined,
                keys = undefined,
                i = undefined,
                len = undefined;
    
            // Make Super available to returned func.
            var Super = arguments[0];
            return function(/* func */){
                /* This redefines the function with the current execution context.
                 * Meaning that when the returned function is called it will have all of the current scopes variables available to it, which right here is just "Super"
                 * This has the unfortunate side effect of ripping the old execution context away from the method meaning that no private methods that may have been defined in the original scope are available to it.
                 */
                return eval("("+ arguments[0] +")");
            };
        };
    
        var parentSuper = superify(parent.prototype);
    
        var oldProto = child.prototype;
        var keys = Object.getOwnPropertyNames(oldProto);
        child.prototype = Object.create(parent.prototype);
        Object.defineProperty(child.prototype, "constructor", {enumerable: false, value: child});
    
        for(var i = 0, len = keys.length; i<len; i++)
            if("function" === typeof oldProto[keys[i]])
                child.prototype[keys[i]] = parentSuper(oldProto[keys[i]]);
    }
    

    An example of making a class

    function P(){}
    P.prototype.logSomething = function(){console.log("Bro.");};
    
    function C(){}
    C.prototype.logSomething = function(){console.log("Cool story"); Super.logSomething.call(this);}
    
    extend(C, P);
    
    var test = new C();
    test.logSomething(); // "Cool story" "Bro."
    

    An example of the drawback mentioned earlier.

    (function(){
        function privateMethod(){console.log("In a private method");}
    
        function P(){};
    
        window.C = function C(){};
        C.prototype.privilagedMethod = function(){
            // This throws an error because when we call extend on this class this function gets redefined in a new scope where privateMethod is not available.
            privateMethod();
        }
    
        extend(C, P);
    })()
    
    var test = new C();
    test.privilagedMethod(); // throws error
    

    Also note that this method isn't "superifying" the child constructor meaning that Super isn't available to it. I just wanted to explain the concept, not make a working library :)

    Also, just realized that I met all of OP's conditions! (Although there really should be a condition about execution context)

    0 讨论(0)
  • 2020-12-08 11:12

    I don't think there is a "free" way out of the "recursive super" problem you mention.

    We can't mess with the this because doing so would either force us to change prototypes in a nonstandard way, or move us up the proto chain, losing instance variables. Therefore the "current class" and "super class" must be known when we do the super-ing, without passing that responsibility to this or one of its properties.

    There are many some things we could try doing but all I can think have some undesireable consequences:

    • Add super info to the functions at creation time, access it using arguments.calee or similar evilness.
    • Add extra info when calling the super method

      $super(CurrentClass).method.call(this, 1,2,3)
      

      This forces us to duplicate the current class name (so we can look up its superclass in some super dictionary) but at least it isn't as bad as having to duplicate the superclass name, (since coupling against the inheritance relationships if worse then the inner coupling with a class' own name)

      //Normal Javascript needs the superclass name
      SuperClass.prototype.method.call(this, 1,2,3);
      

      While this is far from ideal, there is at least some historical precedent from 2.x Python. (They "fixed" super for 3.0 so it doesn't require arguments anymore, but I am not sure how much magic that involved and how portable it would be to JS)


    Edit: Working fiddle

    var superPairs = [];
    // An association list of baseClass -> parentClass
    
    var injectSuper = function (parent, child) {
        superPairs.push({
            parent: parent,
            child: child
        });
    };
    
    function $super(baseClass, obj){
        for(var i=0; i < superPairs.length; i++){
            var p = superPairs[i];
            if(p.child === baseClass){
                return p.parent;
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题