26

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

  • this.$super.prop()
  • this.$super.prop.apply(this, arguments);

Criteria to uphold are :

  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
  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.
  3. You must not hardcode references to super objects in your children. I.e. Base.prototype.f.apply(this, arguments); defeats the point.
  4. You must not use a X to JavaScript compiler or JavaScript preprocessor.
  5. Must be ES5 compliant

The naive implementation would be something like this.

var injectSuper = function (parent, child) {
  child.prototype.$super = parent.prototype;
};

But this breaks condition 2.

The most elegant mechanism I've seen to date is IvoWetzel's eval hack, which is pretty much a JavaScript preprocessor and thus fails criteria 4.

Ted Naleid
  • 26,511
  • 10
  • 70
  • 81
Raynos
  • 166,823
  • 56
  • 351
  • 396
  • 1
    just curious, but is there a reason *why* you need to emulate `super` in JavaScript? given the way the prototype chain works, it strikes me as completely unnecessary. – zzzzBov Nov 07 '11 at 05:28
  • @zzzzBov `super` is simply a construct for code re-use. It's the only thing I'm missing in OO sugar for ES5. ES6 brings `super` and I'm looking forward to that – Raynos Nov 07 '11 at 12:11
  • Your naive implementation also breaks criteria 1. – Thomas Eding Nov 07 '11 at 18:48
  • 1
    `injectSuper(P, C); x = new C(); C.prototype = {}; alert(x.$super.constructor === P); y = new C(); alert(y.$super === undefined);` Emits true for both. – Thomas Eding Nov 07 '11 at 19:24
  • @trinithis of course it does. condition one only needs to reflect changes to `P`. If you overwrite `C.prototype` then its break. this is expected behaviour and it's plain silly to write code like that – Raynos Nov 07 '11 at 19:34
  • I misunderstood what you meant by changing the super prototype. It can be understood in the way I originally thought in the sense that you are literally changing it to something else instead of modifying what is already there. – Thomas Eding Nov 07 '11 at 19:37
  • I love how you criticize every answer but you really have no idea how to do it yourself. Talk about constructive criticism uh? – Pablo Fernandez Nov 07 '11 at 23:45
  • @PabloFernandez o/ Yes I'm harsh. I've had a good look at how to do this, and I found a whole bunch of pitfalls. I actively listed those pitfalls and other people still step into them. So yes they get to be criticised. – Raynos Nov 08 '11 at 00:06
  • @Raynos - maybe this is why most give up on emulating "classic" inheritance in javascript. You end up tying the code in knots to meet criteria for classic OO without actually solving a pracitcal issue (unless the issue was to implement classic OO for the sake of it). – RobG Nov 08 '11 at 04:40
  • @RobG It's not "classic" inheritance as such, more of a code re-use mechanism. And it can be done, you just have to overwrite every single method to lexically bind it to a sensible value of super. It's basically trying to emulate ES6 `super`. – Raynos Nov 08 '11 at 09:48
  • I made code that passed all the tests and he stops responding in comments... Downvote – AutoSponge Nov 09 '11 at 00:26
  • You can use `Function.call` easily in ECMAScript5, have a look here: http://stackoverflow.com/questions/7486825/javascript-inheritance/12816953#12816953 – Lorenzo Polidori Jun 29 '13 at 08:54

11 Answers11

10

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;
        }
    }
}
hugomg
  • 68,213
  • 24
  • 160
  • 246
  • Oh I like coupling with the class name rather then the superclass name. – Raynos Nov 07 '11 at 20:31
  • You can avoid the CurrentClass if you have a closure over $super with the superclass. In both cases, you eventually have to know the superclass, so why not? – Thomas Eding Nov 07 '11 at 20:36
5

John Resig posted an ineherence mechanism with simple but great super support. The only difference is that super points to the base method from where you are calling it.

Take a look at http://ejohn.org/blog/simple-javascript-inheritance/.

ngryman
  • 7,112
  • 2
  • 26
  • 23
  • Oh dear god that's ugly. He overwrites the functions and changes the value of `_super` before and after you call the method. – Raynos Nov 07 '11 at 12:14
  • To be honest, what's wrong with ugly when it is done under the hood. Of course, Resig is missing a try-catch block in there. As it is, if an exception is thrown from a superclass method, you're f*cked. – Thomas Eding Nov 07 '11 at 18:15
  • 1
    The appropriate fix to the exception problem: `try { var ret = fn.apply(this, arguments); } catch (e) { this._super = tmp; throw e; } this._super = tmp;` – Thomas Eding Nov 07 '11 at 18:18
  • What I don't like about this implementation is that you can only `_super` into a method of the same name. Boo. – Thomas Eding Nov 07 '11 at 20:30
  • 3
    Another problem with Resig's code. Suppose you have classes `P` and `C`, where `C` inherits from `P`. Also suppose `P` and `C` both define their own `foo` and `bar` methods on their prototypes. Lastly, suppose `P.foo` calls `bar` and `C.foo` calls `_super`. When calling `new C().foo()`, `C.bar` gets called, not `P.bar`. Unless you are really clever, this is a bug. I haven't even tried mutual recursion between parent and child class methods. I bet that is even nastier in that I bet it utterly breaks his `_super` mechanism. – Thomas Eding Nov 07 '11 at 22:02
  • 1
    @trinithis: JR's code is not perfect for sure. But for ~25 lines of code it is enough to emulate a good inherence paradigm that fits 90% of the needs. For the case you have described, a patch is necessary. – ngryman Nov 08 '11 at 08:49
  • 1
    Well, I think it is more like 30%, but okay. – Thomas Eding Nov 08 '11 at 09:29
2

Note that for the following implementation, when you are inside a method that is invoked via $super, access to properties while working in the parent class never resolve to the child class's methods or variables, unless you access a member that is stored directly on the object itself (as opposed to attached to the prototype). This avoids a slew of confusion (read as subtle bugs).

Update: Here is an implementation that works without __proto__. The catch is that using $super is linear in the number of properties the parent object has.

function extend (Child, prototype, /*optional*/Parent) {
    if (!Parent) {
        Parent = Object;
    }
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
    for (var x in prototype) {
        if (prototype.hasOwnProperty(x)) {
            Child.prototype[x] = prototype[x];
        }
    }
    Child.prototype.$super = function (propName) {
        var prop = Parent.prototype[propName];
        if (typeof prop !== "function") {
            return prop;
        }
        var self = this;
        return function () {
            var selfPrototype = self.constructor.prototype;
            var pp = Parent.prototype;
            for (var x in pp) {
                self[x] = pp[x];
            }
            try {
                return prop.apply(self, arguments);
            }
            finally {
                for (var x in selfPrototype) {
                    self[x] = selfPrototype[x];
                }
            }
        };
    };
}

The following implementation is for browsers that support the __proto__ property:

function extend (Child, prototype, /*optional*/Parent) {
    if (!Parent) {
        Parent = Object;
    }
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
    for (var x in prototype) {
        if (prototype.hasOwnProperty(x)) {
            Child.prototype[x] = prototype[x];
        }
    }
    Child.prototype.$super = function (propName) {
        var prop = Parent.prototype[propName];
        if (typeof prop !== "function") {
            return prop;
        }
        var self = this;
        return function (/*arg1, arg2, ...*/) {
            var selfProto = self.__proto__;
            self.__proto__ = Parent.prototype;
            try {
                return prop.apply(self, arguments);
            }
            finally {
                self.__proto__ = selfProto;
            }
        };
    };
}

Example:

function A () {}
extend(A, {
    foo: function () {
        return "A1";
    }
});

function B () {}
extend(B, {
    foo: function () {
        return this.$super("foo")() + "_B1";
    }
}, A);

function C () {}
extend(C, {
    foo: function () {
        return this.$super("foo")() + "_C1";
    }
}, B);


var c = new C();
var res1 = c.foo();
B.prototype.foo = function () {
    return this.$super("foo")() + "_B2";
};
var res2 = c.foo();

alert(res1 + "\n" + res2);
Thomas Eding
  • 35,312
  • 13
  • 75
  • 106
  • `.__proto__` is non-standard. I'm afraid I can't use this solution. – Raynos Nov 07 '11 at 20:21
  • I'll try to get some more ideas and post 'em if I do. – Thomas Eding Nov 07 '11 at 20:37
  • Your using the same overwrite function and change the value of super before and after trick that john resig used. Except it's slightly more confusing – Raynos Nov 07 '11 at 21:32
  • @Raynos: Resig's code does not allow you to call an arbitrary function from the superclass. Also consider the following: have `foo` and `bar` defined in both the parent `P` and child class `C`. `P.foo` calls `bar`. Now suppose `C.foo` calls `foo` using the super mechanism. I haven't tested it, but I believe Resig's code will have `P.foo` call `C.bar`. This is dicey IMO. I am not going to write a test case, but if anyone does, please let me know the result. – Thomas Eding Nov 07 '11 at 21:43
  • 1
    Okay, I tested it. Resig's code has the flaw I surmised, which I think it is very undesirable. – Thomas Eding Nov 07 '11 at 21:49
  • Changed code to use `Object.create` when creating the prototype. – Thomas Eding Nov 07 '11 at 22:23
  • I kind of liked the `__proto__` version. Its the only way to really imitate the `super` behaviour from the classical languages... – hugomg Nov 08 '11 at 02:18
  • There are a few issues. Like if an object has an own property "foo" and the prototype has a property "foo" then that own property won't be put back because you only put the values on selfPrototype back. The other issue is dealing with non-enumerable properties in the prototype chain – Raynos Nov 09 '11 at 04:27
  • Yeah, the first point is a problem. You can mitigate it (or at least most of it) by storing the original properties and then restoring them after. (Might want to only restore functions, but idk.) Of course this has problems with mutation occurring in the super methods. No solution I think can deal with 100% of the problems. But you can get close, especially if you avoid doing stuff like that. As for non-enumerable properties, I don't think this is an issue unless dealing with arrays as your object, as I imagine most other non-enum props have the same value. I could be wrong here though. – Thomas Eding Nov 09 '11 at 04:52
  • I'm loving this solution. What pattern would you recommend for calling the parent constructor from within the child constructor? – joelpt Jun 21 '12 at 16:33
  • @joelpt: I usually do `Parent.call(this, arg1, arg2, ...)` in the `Child` constructor's first line. – Thomas Eding Jun 21 '12 at 16:54
  • Seems like the following will not work? `const sleep = t => new Promise(r => setTimeout(r, t)); B.prototype.foo = async function () { await sleep(1); return this.$super("foo")() + "_B2"; };` – trusktr Mar 21 '18 at 07:53
  • This is 2018 now... let's use ES6 classes now (and transpilers if needed). – Thomas Eding Mar 24 '18 at 18:46
2

The main difficulty with super is that you need to find what I call here: the object that contains the method that makes the super reference. That is absolutely necessary to get the semantics right. Obviously, having the prototype of here is just as good, but that doesn’t make much of a difference. The following is a static solution:

// Simulated static super references (as proposed by Allen Wirfs-Brock)
// http://wiki.ecmascript.org/doku.php?id=harmony:object_initialiser_super

//------------------ Library

function addSuperReferencesTo(obj) {
    Object.getOwnPropertyNames(obj).forEach(function(key) {
        var value = obj[key];
        if (typeof value === "function" && value.name === "me") {
            value.super = Object.getPrototypeOf(obj);
        }
    });
}

function copyOwnFrom(target, source) {
    Object.getOwnPropertyNames(source).forEach(function(propName) {
        Object.defineProperty(target, propName,
            Object.getOwnPropertyDescriptor(source, propName));
    });
    return target;
};

function extends(subC, superC) {
    var subProto = Object.create(superC.prototype);
    // At the very least, we keep the "constructor" property
    // At most, we preserve additions that have already been made
    copyOwnFrom(subProto, subC.prototype);
    addSuperReferencesTo(subProto);
    subC.prototype = subProto;
};

//------------------ Example

function A(name) {
    this.name = name;
}
A.prototype.method = function () {
    return "A:"+this.name;
}

function B(name) {
    A.call(this, name);
}
// A named function expression allows a function to refer to itself
B.prototype.method = function me() {
    return "B"+me.super.method.call(this);
}
extends(B, A);

var b = new B("hello");
console.log(b.method()); // BA:hello
Axel Rauschmayer
  • 25,381
  • 5
  • 24
  • 17
  • Your static solution is a reasonable example, but it's not clear how your generalize it since you hard coded `"me"` into it. I'd also argue that `super(B).method` is nicer then `me.super.method`. – Raynos Nov 15 '11 at 14:28
  • Also if B shadows a method of A and `A.prototype.method` refers to that method, should A's or B's method be invoked? Which one is correct? – Raynos Nov 15 '11 at 14:32
  • @Raynos: “me” only exists inside the function, it thus also works for several methods. Try: `(function me() { console.log(me) }()); console.log(me);` – Axel Rauschmayer Nov 15 '11 at 15:04
  • @Raynos: As to whether A’s or B’s method should be used – that depends. The norm in OO programming is B’s method (because it overrides). If you want to do a “sideways reference”, you probably should use a function in a closure, instead. – Axel Rauschmayer Nov 15 '11 at 15:12
1

JsFiddle:

What is wrong with this?

'use strict';

function Class() {}
Class.extend = function (constructor, definition) {
    var key, hasOwn = {}.hasOwnProperty, proto = this.prototype, temp, Extended;

    if (typeof constructor !== 'function') {
        temp = constructor;
        constructor = definition || function () {};
        definition = temp;
    }
    definition = definition || {};

    Extended = constructor;
    Extended.prototype = new this();

    for (key in definition) {
        if (hasOwn.call(definition, key)) {
            Extended.prototype[key] = definition[key];
        }
    }

    Extended.prototype.constructor = Extended;

    for (key in this) {
        if (hasOwn.call(this, key)) {
            Extended[key] = this[key];
        }
    }

    Extended.$super = proto;
    return Extended;
};

Usage:

var A = Class.extend(function A () {}, {
    foo: function (n) { return n;}
});
var B = A.extend(function B () {}, {
    foo: function (n) {
        if (n > 100) return -1;
        return B.$super.foo.call(this, n+1);
    }
});
var C = B.extend(function C () {}, {
    foo: function (n) {
        return C.$super.foo.call(this, n+2);
    }
});

var c = new C();
document.write(c.foo(0) + '<br>'); //3
A.prototype.foo = function(n) { return -n; };
document.write(c.foo(0)); //-3

Example usage with privileged methods instead of public methods.

var A2 = Class.extend(function A2 () {
    this.foo = function (n) {
        return n;
    };
});
var B2 = A2.extend(function B2 () {
    B2.$super.constructor();
    this.foo = function (n) {
        if (n > 100) return -1;
        return B2.$super.foo.call(this, n+1);
    };
});
var C2 = B2.extend(function C2 () {
    C2.$super.constructor();
    this.foo = function (n) {
        return C2.$super.foo.call(this, n+2);
    };
});

//you must remember to constructor chain
//if you don't then C2.$super.foo === A2.prototype.foo

var c = new C2();
document.write(c.foo(0) + '<br>'); //3
Bill Barry
  • 3,423
  • 2
  • 24
  • 22
1

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

monastic-panic
  • 3,987
  • 1
  • 22
  • 20
0

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)

BAM5
  • 341
  • 2
  • 6
0

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)

Community
  • 1
  • 1
trusktr
  • 44,284
  • 53
  • 191
  • 263
0

For those who do not understand the recursion problem the OP presents, here is an example:

function A () {}
A.prototype.foo = function (n) {
    return n;
};

function B () {}
B.prototype = new A();
B.prototype.constructor = B;
B.prototype.$super = A.prototype;
B.prototype.foo = function (n) {
    if (n > 100) return -1;
    return this.$super.foo.call(this, n+1);
};

function C () {}
C.prototype = new B();
C.prototype.constructor = C;
C.prototype.$super = B.prototype;
C.prototype.foo = function (n) {
    return this.$super.foo.call(this, n+2);
};


alert(new C().foo(0)); // alerts -1, not 3

The reason: this in Javascript is dynamically bound.

Thomas Eding
  • 35,312
  • 13
  • 75
  • 106
  • 1
    This `B.prototype = new A();` is pretty bad to me. – ZenMaster Nov 07 '11 at 05:51
  • @ZenMaster: What else would you do? That is how you do prototypal inheritance in JS. Of course, I would have some sort of function that wraps the functionality, but the concept is the same. That said, I just realized that my $super is implemeneted in a way that breaks condition 1. I can't think of a fix at the moment. – Thomas Eding Nov 07 '11 at 16:58
  • @trinithis the correct thing is `B.prototype = Object.create(A.prototype)` – Raynos Nov 07 '11 at 21:50
  • It's only a problem if, in the example, you are expecting *this* within *B.prototype.foo* to reference an instance of *B*, whereas it references an instance of *C*. Isn't static binding available in ES5 with *Function.prototype.bind*? Or you could use a closure. – RobG Nov 08 '11 at 04:59
  • 1
    Closures won't solve the problem. Bind won't solve the problem. It has to do with `this` always containing references to the child class's methods. – Thomas Eding Nov 08 '11 at 18:51
  • There are issues with this as well, so I'm not recommending it, but because no one else suggested it.. Try switching those `this.$super.foo.call(this, ...)` calls to `this.$super.foo.call(this.$super, ...)`. And you should get 3, and going by the Class.extend example above, switching A to return -n should also get you -3. Again, not suggesting this because you lose your C instance when inside B and A and that may not be what you expect, I just wanted to illustrate a path that makes that call chain work. :) – Jasmine Hegman May 01 '13 at 21:01
0

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

ThiefMaster
  • 310,957
  • 84
  • 592
  • 636
  • Ew! That overwrites methods with new functions which set the value of `this.$super` to the "correct value" before and after. It also swallows errors :( – Raynos Nov 07 '11 at 12:18
-1

I think I have a more simple way....

function Father(){
  this.word = "I'm the Father";

  this.say = function(){
     return this.word; // I'm the Father;
  }
}

function Sun(){
  Father.call(this); // Extend the Father

  this.word = "I'm the sun"; // Override I'm the Father;

  this.say = function(){ // Override I'm the Father;
    this.word = "I was changed"; // Change the word;
    return new Father().say.apply(this); // Call the super.say()
  }
}

var a = new Father();
var b = new Sun();

a.say() // I'm the father
b.ay() // I'm the sun
b.say() // I was changed
  • 1
    -1: That answer is not very useful. It does not use prototyping at all. Esp. the `new Father().say.apply(this)` work around is terrible. – Kijewski Jun 18 '13 at 22:18
  • The spirit of what you are trying to say is correct. Your implementation is far from being close. With the understanding that this is an extremely contrived example, a correct implementation is illustrated in [this jsbin](http://jsbin.com/vegez/1/edit?js,console) – Sukima Apr 30 '14 at 12:30