The `super` keyword doesn't work as it should?

# /#!/JoePea (8 years ago)

For example, both of these examples don't work in Chrome:

function A() {
    console.log('A')
}
A.prototype.constructor = A
A.prototype.hello = function() {
    return 'hello'
}

function B() {
    console.log('B')
    // super()
    A.call(this)
}
B.prototype = Object.create(A.prototype)
B.prototype.constructor = B
B.prototype.hello = function() {
    return super.hello() + 'there'
}

new B

and

let obj1 = {
    hello() {
        return 'hello'
    },
    sayHello() {
        console.log(this.hello())
    }
}

console.log('Obj1 says hello:')
obj1.sayHello()

let obj2 = Object.create(obj1)
Object.assign(obj2, {
    hello() {
        return super.hello() + 'there.'
    }
})

console.log('Obj2 says hello:')
obj2.sayHello() // Error

I was hoping super was more flexible than that.

For example, in the first snippet, why can't super simply be a shortcut for "look up the prototype of the object that the method is called on, then find the .constructor property and call it on this"? That seems to be simple. It could throw an error if .constructor is not found, in the case of ES5-style classes that aren't defined using that pattern that ES6 classes are syntax sugar for.

And in the second example, why does super.hello not work as expected?

I believe that these limitations may severely limit my ability to create the multiple-inheritance tool that I'm imagining over at esdiscuss.org/topic/symbol-for-modifying-property-lookup#content-8.

Any ideas or suggestions?

# /#!/JoePea (8 years ago)

Regarding the second example, this resource explains why Object.assign doesn't work as expected.

I believe the VM should be smarter than that, and let end-developers use any technique for inheritance that they wish.

For example,maybe the VM should determine at runtime what the [[HomeObject]] is based on what object the method is called on. Seems like that would make super much more useful.

Doesn't that make more sense? If not, why not?

If this were the case, then the first example would work just fine too.

# Bergi (8 years ago)

/#!/JoePea wrote:

Why can't super simply be a shortcut for "look up the prototype of the object that the method is called on, then find the .constructor property and call it on this"? That seems to be simple.

Simple, yes, and broken in the case of multi-level inheritance:

const x = Object.assign(Object.create({
     method() {
         console.log("parent");
     }
}), {
     method() {
         console.log("child");
         Object.getPrototypeOf(this).method(); // super.method()
     }
});
x.method(); // works as expected

const y = Object.create(x);
y.method(); // infinite loop/stack overflow

A super query must not depend on this (only), it must statically resolve the object on which the called method is defined.

In constructors, using the prototype of the currenctly called constructor for super() works well, but you'd need to use Object.setPrototype as there is currently no declarative way other than classes to define functions with custom prototypes.

In methods, there would need to be a way to populate the [[HomeObject]] other than declaring the method as part of a class/object literal.

Kind , Bergi

# /#!/JoePea (8 years ago)

yes, so the object that super references would work like this, where the value is determined at runtime instead of in a declaration. Basically, super === Object.getPrototypeOf(this) would actually be true in my examples. It may be due to "extra overhead" that [[HomeObject]] is only defined during declaration, but that is at the huge expense of making the language less intuitive and also more difficult to work with in some cases (for example, in designing a multiple-inheritance scheme).

It would simply be great for super to just work as expected in the examples I gave, which would mean that super would work in tandem and intuitively with the various ways in which we can create objects-extending-objects in JS.

Good news is that making the necessary change to super in ES8 or later is completely backwards compatible with how it currently works.

I wonder what the performance problems are and if they can be solved.

# Andrea Giammarchi (8 years ago)

super === Object.getPrototypeOf(this) also doesn't work with multiple inheritance.

If interested, it has been solved dynamically in this good'ol library: WebReflection/es-class#es6

# Logan Smyth (8 years ago)

Joe, it seems like you've focused on super === Object.getPrototypeOf(this) as the overall ideal without considering the issues with it. I've tried to put together a few counterexamples below. Say you have a base set up like this:

var a = {
  prop: null,
  method(){
    this.prop = 4;

    // super.method();
    // vs
    // Object.getPrototypeOf(this).method.call(this);

    return this.prop;
  },
};
var b = {
  __proto__: a,
  method: function(){
    this.prop = 5;

    // super.method();
    // vs
    // Object.getPrototypeOf(this).method.call(this);
  },
};
var c = {
    __proto__: b,
    method: function(){
        this.prop = 6;
    },
};

In this example, super.method() will work fine, and a.method() === 6 because each super call will reassign this.prop, where this === a.

Object.getPrototypeOf(this) has one main core issue here, which is that we are doing .call(this);, meaning that when a.method() is called and subsequently calls b.method, this === a, not this === b inside b.method. This means that when b attempts to call its super class, it has no way of finding c, because this === a and Object.getProtoypeOf(this) === b, not c. This leads to the infinite recursion case that Bergi mentioned.

The only way for this to work, given your proposal, would be to call b.method with this === b instead of this === a, e.g. Object.getPrototypeOf(this).method.call(Object.getPrototypeOf(this));, but that would mean that operations happening inside b.method, like the this.prop = 5; would be assigning a property on the wrong object (b), instead of the a object, leading to a.method() === 4.

ES6 solves this by looking up the parent prototype using the [[HomeObject]] as the root instead of this, where [[HomeObject]] is essentially the object that the function was attached to syntactically. The issue is that a standalone function has no object that it is attached to. This means that usage of super.foo ends up being restricted to only functions written with method syntax, where they are attached clearly to a specific object.

# Raul-Sebastian Mihăilă (8 years ago)

An alternative would be to consider the object where the method key was found as the home object of the method.

# Allen Wirfs-Brock (8 years ago)

On Jul 19, 2016, at 11:45 AM, Raul-Sebastian Mihăilă <raul.mihaila at gmail.com> wrote:

An alternative would be to consider the object where the method key was found as the home object of the method.

that was considered while designing ES6. The problem is that it introduces an additional implicit parameter (or equivalent overhead) for every method call, regardless of whether or not it is actually needed by the invoked method.

# Bergi (8 years ago)

Raul-Sebastian Mihăilă wrote:

An alternative would be to consider the object where the method key was found as the home object of the method.

That's just as error-prone, method borrowing would only work when the two objects had the same superclass. Also, what about methods that are not "found" anywhere when called, for example when used with call/apply/bind? Or static class methods that don't use this at all and are called like a plain function?

Are you suggesting that every property access that yields a function implicitly creates a closure over the "home"/"found" object? That's a no-no for obvious reasons.

Kind , Bergi

# Raul-Sebastian Mihăilă (8 years ago)

I wasn't suggesting any mechanism for accomplishing that. Method borrowing is error prone either way. With the current spec, it's risky because this and super basically may use two different prototype chains. Also you may change the prototype of the home object and run into trouble. (In the alternate approach I would expect a super evaluation in a non-method function call to throw). Anyway, I'm OK with the current spec it doesn't require too much extra discipline.

# /#!/JoePea (8 years ago)

In this example, super.method() will work fine, and a.method() === 6

because each super call will reassign this.prop, where this === a.

Did you mean c.method() === 6? Also I'm assuming you meant for .method of b and c to have return statements too? I also assume you meant to put super vs getPrototypeOf in the c.method too? Because, if calling a.method(), then how can super or getPrototypeOf possibly refer to b orc? a.method() === 4, and assuming return statements in b and c .methods along with the super vs getPrototypeOf after the assignment in c.method as with b, then c.method() === 4, and b.method() === 4.

I believe you meant to give something more like the following example (note, using inline methodsin all objects), which does show the problem with the infinite recursion:

example()
function example() {
    var a = {
        prop: null,
        name: 'a',
        method(){
            console.log('Setting this.prop in', this.name, super.name)
            this.prop = 4;

            // no super call here.

            return this.prop;
        },
    };

    var b = {
        __proto__: a,
        name: 'b',
        method(){
            console.log('Setting this.prop in', this.name, super.name)
            this.prop = 5;

            // super.method();
            // vs
            Object.getPrototypeOf(this).method.call(this);

            return this.prop;
        },
    };

    var c = {
        __proto__: b,
        name: 'c',
        method(){
            console.log('Setting this.prop in', this.name, super.name)
            this.prop = 6;

            // super.method();
            // vs
            Object.getPrototypeOf(this).method.call(this);

            return this.prop;
        },
    };

    c.method()
}

// Output:
// Setting this.prop in c a
// Setting this.prop in c a
// Setting this.prop in c a
// Setting this.prop in c a
// Setting this.prop in c a// ...repeats forever...​

In that example, this is always c, so each round just fires .method from b and calls it on c over and over.

This is perfectly and exactly proof that super needs to work as expected so we don't have to go through great lengths to make runtime-ready (instead of declaration-only) implementations of "super" as needed in my original two examples.

Now here's the same example using super instead of getPrototypeOf, and it works perfectly (because everything is defined at declaration time):

example()
function example() {
    var a = {
        prop: null,
        name: 'a',
        method(){
            console.log('Setting this.prop in', this.name, super.name)
            this.prop = 4;

            // no super call here.

            return this.prop;
        },
    };

    var b = {
        __proto__: a,
        name: 'b',
        method(){
            console.log('Setting this.prop in', this.name, super.name)
            this.prop = 5;

            super.method();
            // vs
            // Object.getPrototypeOf(this).method.call(this);

            return this.prop;
        },
    };

    var c = {
        __proto__: b,
        name: 'c',
        method(){
            console.log('Setting this.prop in', this.name, super.name)
            this.prop = 6;

            super.method();
            // vs
            // Object.getPrototypeOf(this).method.call(this);

            return this.prop;
        },
    };

    c.method()
}

// Output:
// Setting this.prop in c b
// Setting this.prop in c a
// Setting this.prop in c undefined

super just needs to work this way with my examples too, that's all I'm saying, because that would be intuitive. The current behavior is not intuitive.

I also see what you mean about super simply referring to Object.getPrototypeOf(this) causing infinite recursion because this is always the leaf-most object in the prototype chain no matter where in the chain the method is found. Let me revise what I really meant to say:

super should be a reference to the prototype of the current object in the prototype chain where the super statement is running from. So, at the leaf-most object, super === Object.getPrototypeOf(this). After that, in the second-to-leaf-most object, super === Object.getPrototypeOf(Object.getPrototypeOf(this)). In the third-to-leaf-most object super === Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(this))).

And so on... Or, in other words, [[HomeObject]] for a method() should just be equivalent to o in o.method() if o is a reference to the current object in a prototype chain where method() is found. Get what I mean?

To further prove the point that super doesn't work as intuitively expected, this comment, this comment, and this comment all make the assumption that super is Object.getPrototypeOf(this) (which is wrong anyways as you pointed with the infinite recursion, and I'm willing to bet they were actually thinking more along the lines of the behavior I just described in the last paragraph, although if super worked like that in the first place, the source of confusion would never make itself present). And these comments are on the very first article that I read. There are probably other examples of confusion in comments of articles on super.

TLDR: super isn't so super, and I wish it was for the sake of clarity, intuition, and meta-programming flexibility. My currently imagined implementation of multiple-inheritance does not work because super doesn't work the way I'm intuitively expecting due reliance on a static [[HomeObject]] defined only during declaration.

I'm continuing to try to make my implementation work until I've exhausted all possibilities. For reference, the following is what I wish for the API to be like. If anyone has any suggestion on any direction to try, it would be much appreciated. :]

import multiple from 'somewhere-yet-to-be' // multi-inheritance tool
import {DOMMatrix} from 'geometry-interfaces'

// Node, these classes do not extend anything.
class ImperativeBase {}
class TreeNode {}

// Single-inheritance:
class Transformable extends DOMMatrix {}

// Multiple-inheritance:
class Node extends multiple(ImperativeBase, TreeNode, Transformable) {}
class Scene extends multiple(ImperativeBase, TreeNode) {}
# medikoo (8 years ago)

Joe, see this post: mozilla.6506.n7.nabble.com/Making-quot-super-quot-work-outside-a-literal-td90457.html#a90551

There are some explanations there on why super was implemented as something /static/ and not /dynamic/

-- View this message in context: mozilla.6506.n7.nabble.com/The-super-keyword-doesn-t-work-as-it-should-tp357032p357113.html Sent from the Mozilla - ECMAScript 4 discussion mailing list archive at Nabble.com.

# Andrea Giammarchi (8 years ago)

For implementation sake, this is a quick'n'dirty approach that brings a dynamically resolved super to any object or prototype: gist.github.com/WebReflection/ee4695c107339e039878b02afb90dc0d

Usage example:

function A() {
  console.log(this.constructor.name);
}
A.prototype.method = function () {
  console.log('method from ' + this.constructor.name);
};

function B() {
  this.super();
  console.log(this.constructor.name);
}
B.prototype = Object.create(A.prototype, {constructor: {value: B}});
withSuperContext(B.prototype).method = function () {
  this.super.method();
  console.log('method from ' + this.constructor.name);
};

function C() {
  this.super();
  console.log(this.constructor.name);
}
C.prototype = withSuperContext(
  Object.create(B.prototype, {constructor: {value: C}})
);
C.prototype.method = function () {
  this.super.method();
  console.log('method from ' + this.constructor.name);
};

var c = new C;
// will log
// A
// B
// C

c.method();
// method from A
// method from B
// method from C

Best

# Logan Smyth (8 years ago)

Joe, yes sorry, my mistake. a should have __proto__: b, and b should have __proto__: c in my example, that's what I get for not validating it better. Each could return but since a.method was the only one I called, it was the only one I put the return in.

# /#!/JoePea (8 years ago)

@medikoo, wow, the conversation has been going on for a long time. And in that conversation you linked, Sean Eagan said

I think a static |super| in light of ​ ​ES's dynamic |this| would

actually be much more​ ​surprising. This would lead to looking for properties in a static |super| object that may be​ ​completely unrelated to the dynamic |this| value of a given function activation, which would​ ​certainly be surprising.

Consistency with other languages is valuable, but consistency with this

language (ES) is vital. A static |super| would be inconsistent with ES's dynamic |this|.

​I couldn't agree more.​ ​ ​ ​

The interesting thing is although the conversation is so old, super is only just now coming out in JS engines. I only just now discovered this undesirable behavior because ES6 only just now became reality.

Is there any hard evidence of the performance cost of a dynamic super? So far I've read in various places (for example the thread linked to by @medikoo, Axel's 2ality article on super) about there being "overhead", but the articles haven't quantified or put into perspective the actual performance cost. Is it really that bad?

# medikoo (8 years ago)

/#!/JoePea wrote

Is there any hard evidence of the performance cost of a dynamic super? So far I've read in various places (for example the thread linked to by @medikoo, Axel's 2ality article on super) about there being "overhead", but the articles haven't quantified or put into perspective the actual performance cost. Is it really that bad?

Adding any additional internal property for function that needs to be updated depending on circumstances, definitely will bring noticeable performance regression, and no-one would want to agree for that.

Still, for me (at least now) it's not clear why it actually implies any cost (?) If /dynamic/, the super should bring not cost, as in clear thinking it's just pure keyword which forces no action/calculation from compiler/interpreter until the moment it's evaluated. It'll be great if someone clarifies that.

-- View this message in context: mozilla.6506.n7.nabble.com/The-super-keyword-doesn-t-work-as-it-should-tp357032p357148.html Sent from the Mozilla - ECMAScript 4 discussion mailing list archive at Nabble.com.

# Claude Pache (8 years ago)

There is probably no way to make the super semantics just work in all use cases. But I think that the choice made in ES6 is the right one in order to have it correctly working in the most naïve (and maybe the most common?) use cases of method borrowing, i.e., when you don’t have knowledge of — or even you wilfully ignore — its implementation details. Consider for example:

class MyArray extends Array {
    forEach(callback, thisArg) {
        // ...
        super.forEach(callback, thisArg)
        // ...
    }
}

and somewhere else:

NodeList.prototype.forEach = MyArray.prototype.forEach // or: (new MyArray).forEach

HTMLCollection.prototype.forEach = MyArray.prototype.forEach

Here, I expect that super.forEach will continue to point to Array.prototype.forEach. This is because I’m thinking of MyArray.prototype.forEach as a black box that should not change its behaviour just because I’m moving it around, as it is generally the case with methods of the builtin library.

# /#!/JoePea (8 years ago)

It doesn't make sense because then super isn't really referring to the super class that the method is used on. I think it makes more sense for super to take meaning depending on where the method is found, not where it is defined.

What if there was also something like Function.prototype.bind like Function.prototype.with, so someFunc.with(homeObject) returns a new function who's [[HomeObject]] is the specified homeObject. It would be possible to do someFunc.with(...).bind(...) to configure both the home object and this.

# Allen Wirfs-Brock (8 years ago)

On Jul 24, 2016, at 7:04 PM, /#!/JoePea <joe at trusktr.io> wrote:

What if there was also something like Function.prototype.bind like Function.prototype.with, so someFunc.with(homeObject) returns a new function who's [[HomeObject]] is the specified homeObject. It would be possible to do someFunc.with(...).bind(...) to configure both the home object and this.

This was included in the ES6 drafst for quite awhile. Initially with the name defineMethod and latter as toMethod. But it was eventually decided to remove it. The pros and cons of such a function were extensively discussed with in TC39 over a several year period. For example, see tc39/tc39-notes/blob/master/es6/2014-01/jan-28.md#more-on-tomethod, tc39/tc39-notes/blob/master/es6/2014-01/jan-28.md#more-on-tomethod

The issues with defineMethod/toMethod are summarized in tc39/tc39-notes/blob/master/es6/2015-03/mixin-proposal.pdf, tc39/tc39-notes/blob/master/es6/2015-03/mixin-proposal.pdf and allenwb/ESideas/blob/master/dcltomethod.md, allenwb/ESideas/blob/master/dcltomethod.md which is a proposal for an alternative way to address the problem. Notes from that discussion are at tc39/tc39-notes/blob/master/es6/2015-03/mar-25.md#6iv-a-declarative-alternative-to-tomethod-allen-wirfs-brock, tc39/tc39-notes/blob/master/es6/2015-03/mar-25.md#6iv-a-declarative-alternative-to-tomethod-allen-wirfs-brock

Where it has been left by TC30 is roughly:

  1. There are indeed use cases for dynamically configuring the HomeObject binding but they are quite specialized.
  2. All of the solution that have been proposed are confusing or error prone.
  3. So far, the actual user demand for such a feature is small.
  4. We aren’t going to do anything until there is significant real world feedback saying that it really is needed.
# /#!/JoePea (8 years ago)

With the current spec, it's risky because this and super basically may

use two different prototype chains

That's horrible, and I think that should be possible only when forced, by manually using Function.prototype.toMethod. Otherwise (with super being dynamic) there should be only a single prototype chain in question. It should not even be a concern.

# /#!/JoePea (8 years ago)
  1. There are indeed use cases for dynamically configuring the HomeObject

binding but they are quite specialized

I suppose my case is quite specialized. I am wanting to duplicate the prototype chains of ES6 classes, but I have no reliable (afaik) way to make the super keyword work on copied methods. If I copy the method by reference then the super keyword is based on the wrong HomeObject. If I copy the method by getting its the source with .toString() followed by using eval to define the new method on an object initialization, that works and super will work, but this has problems if the original method relied on variables in the outer scope where it was originally defined as the copied method will not have access to that scope (f.e. code transpiled by Babel may not be able to see the Babel helper functions).

I am trying to implement a function multiple() so that I can do

class SomeClass { ... }
classOtherClass{ ... }
class Foo extends multiple(SomeClass, OtherClass) { ... }

and it's proving to be really difficult because I can't configure HomeObject. The multiple function copies the prototype chains of each class, then combines them together (when possible, otherwise an error is thrown, depending on the native prototypes involved). As mentioned, this somewhat works using eval() in the implementation, except that copied methods don't work if they rely on variables of the outer scope where they were originally defined. For example, suppose SomeClass in the following example was imported from another file, and it depends on Babel helpers defined in the original scope where it was imported from:

import SomeClass from './somewhere/SomeClass'
classOtherClass{ ... }
class Foo extends multiple(SomeClass, OtherClass) { ... }

The multiple() call will copy the methods of SomeClass onto a new prototype chain (the methods need new HomeObjects), but the new methods will fail to reference anything from the original scope where SomeClass came from.

I need to be able to modify HomeObjects without using an eval() hack in order for my multiple() implementation to work 100% as intended.

For now I may need to abandon my effort and use class-factory "mixins" as in

let SomeMixin = baseClass => class SomeMixin extends baseClass { ... }

, but the downside of this is that the classes are not usable as standalone classes, so and end-user can't do

class Foo extends SomeClass { ... }

We are required to do

class Foo extends SomeClass(null) { ... }

but due to the null the class we won't have Object methods. We could do

class Foo extends SomeClass(Object) { ... }

, but that is not also ideal as Object is not guaranteed to be the native Object; window.Object could be overridden.

I really want to define plain ES6 classes and use 3rd party plain ES6 classes (not class factories). It means that those classes are not limited to being used as mixins.

For reference, here's the implementation so far (with the ugly caveats of cloning methods with eval): gist.github.com/trusktr/8c515f7bd7436e09a4baa7a63cd7cc37

I was planning to add caching so that combinations of classes could be re-used (similar to what mixwith.js does to cache mixin applications).

We aren’t going to do anything until there is significant real world

feedback saying that it really is needed.

Here's that feedback. :D

TLDR, it seems that without flexibility in modifying [[HomeObject]] in order to control what super references, I cannot implement an ideal multiple()-inheritance helper that I would be able to comfortably suggest for use in a production app. I would definitely not recommend the my eval-based implementation as it will fail in ways that are very undesirable and unexpected.

With the limitation of a static super, I can't do something simple like use Object.assign to simply copy some methods from one class' prototype onto the class where I need them. That's normal pre-ES6 stuff that has worked along with a dynamic this since forever, so the change in behavior from this to super doesn't fit with the pre-ES6 language that we are previously accustomed to and that we love (I do). ES6+ is backwards-incompatible with some pre-ES6 paradigms.

Pleeeease, let's add something like toMethod, or make super dynamic (preferably both). I don't see any downside to having something like Function.prototype.toMethod, as it is an opt-in type of feature, so it won't bring with it performance loss like a dynamic super might (I still haven't heard conclusive evidence regarding those performance losses).

From what I can imagine, property lookup checks checks the current object,

then goes to the next prototype and does the same, until finally it reaches a prototype where the property is found. Once that object (a HomeObject) is found, we know what the HomeObject is. It seems very easy to call a found method with that found HomeObjectargument, and when the method is called and if it uses super, then a similar search will happen up the prototype chain until the matching super property is found. The second prototype search (the one initiated due to the method using super) is already a requirement, so, this means that the extra overhead for a dynamic super stems from simply passing as argument the HomeObject of a method once the method is found on a prototype chain. A static super means that the [[HomeObject]] variable is simply defined forever instead of being marked in the property-lookup of a method, and that doesn't seem like tons of extra overhead. Note that the property lookup is going to happen anyways, so why not mark the HomeObject during the property lookup algorithm?

Is that really enough overhead to merit a static super? If someone can explain it (or link to it), that would be greatly appreciated.

I also think having dynamic super (or at least a way to configure it) opens up language possibilities that can make interesting things happen (for example, my multiple()-inheritance implementation would work smoothly and instanceof checks would also work thanks to @@hasInstance). The mere fact that a dynamic or configurable [[HomeObject]] would allow for the creation of a tool like what I am trying to implement is good enough reason to have the feature. Who knows what other awesome ideas people will come up with.

# /#!/JoePea (8 years ago)

For now, I've settled with writing classes like this:

const SomeClassMixin = base => {
    class SomeClass extends base {
        // ...
    }

    return SomeClass
}

const SomeClass = SomeClassMixin(class{})
SomeClass.mixin = SomeClassMixin

export {SomeClass as default}

This makes it possible for an end user to import the class and extend from it like normal:

import SomeClass from './SomeClass'

class OtherClass extends SomeClass {
    // ...
}

But, it also allows and end user to use it as a mixin:

import SomeClass from './SomeClass'
import AnotherClass from './AnotherClass'

class OtherClass extends SomeClass.mixin(AnotherClass) {
    // ...
}

This has two main downsides:

  • The class creator has to write the class as a class-factory mixin, then remember to export the mixin application on a plain class {}
  • It is not possible to pass specific arguments to the constructors of each class.

One of my implementations of multiple() does allow one to pass arguments to each constructor. I'll re-visit that when Proxy is supported in Safari.

I can't think of any way to make this work using a class helper library, because those need to create prototype chains, and thus super cannot be modified unless using eval, in which case original scope is lost.

I literally think that a dynamic super and/or Function.prototype.toMethod would allow me to implement the ideal solution, which would have the following benefits:

I am going to leave this alone for now, and just continue with the class-factory mixin approach, although that is not my ideal solution.

Any ideas or suggestions?

# /#!/JoePea (8 years ago)

For reference, here's the implementation of multiple() where multiple constructors calls are possible, using a callSuperConstructor helper: gist.github.com/trusktr/05b9c763ac70d7086fe3a08c2c4fb4bf

# Jordan Harband (8 years ago)

Could you not make multiple be a function that returns a Proxy, rather than achieving multiple inheritance by copying properties?

# /#!/JoePea (8 years ago)

Could you not make multiple be a function that returns a Proxy, rather

than achieving multiple inheritance by copying properties?

​I thought about that originally, so that instead of copying methods onto the class that extends from the multiple super classes, I would just look for the requested properties on each of the leaf-most prototypes of each superclass​. But there's one issue with that: Proxy in Safari isn't released yet. So for now, the copying that I'm doing is a temporary workaround until I re-visit Proxy when Safari 10 comes out, and the MultiClassof my first implementation is basically where I will stick the Proxy (or just replace MultiClass with Proxy if that's cleaner).

There are some problems though:

  • I'm not sure how to handle calling the constructor of each of the multiple sub classes if those classes are ES6 classes, as ES6 class constructors are not callable (but that proposal is abandoned and it also doesn't solve the problem of binding this -- like we can with .call() on regular ES5 functions -- without requiring logic required within both constructor definitions to be moved into a third method), so I'm using new ctor(), then copying the properties from the created objects onto the this of the subclass that derives from the multiple superclasses. Object.assign may not be the best choice there, because we may need to copy descriptors for setters/getters and Symbols too. It could work with ES5 constructors, which are callable, but I haven't written the code to detect ES5 constructors and use .call() on them. It's a rough draft, to see what's feasible.
    • Maybe I can achieve this with Proxy#construct combined with Reflect.construct. Hmmm...
  • It's not possible to use Proxy on the superclass instances while they are being constructed (if they are constructed from ES6 classes) because the objects have to be created and returned from the constructor before a Proxy can be applied around them. It works with ES5 classes though, by calling the constructor with .call(proxy), but the ES6 class constructors are not callable. Is there a way?

Calling methods from the superclasses works fine, as they can simply be bound to the this of the derived class instance (I'm using .bind() currently). If a proxied method is found with the get trap and bound to this of the derived class, then if it reads properties on this then it will end up eventually looking up the property through the Proxies on the subclasses, so that should work.

The benefit of this soon-to-use-actual-Proxy implementation is that super still works because lookup happens on the original prototype chains of the multiple superclasses, not on prototype-chain copies like in the other implementation where copied prototype chains are strung together. In the strung-prototype-chains implementation super doesn't work because it's not configurable without using eval and without accepting the limitations of eval like scope loss on copied methods which is completely unacceptable for a to-be-used-in-production implementation.

Any idea what to do about the constructor calls? Is copying properties after the new objects are created with new ctor() the only way to do it, as far as ES6 class constructors go (I can use .call(proxy) on the ES5 constructors)?

# /#!/JoePea (8 years ago)

Allen, I read your linked documents. After thinking about it more, I believe it would be ideal to have both:

  1. a tool like toMethod. I like "setHome" better as that describes much more concisely what is happening. .bind() is already a way to make a function behave as a method of the object passed to .bind(), so toMethod clashes with that. Also, we're not really assigning a method onto an object with toMethod, so it seems awkward. We're setting the HomeObject, that's what we're doing.
  2. and a dynamic super.

func.bind().toMethod(...) should throw

I believe that .bind and .toMethod are useful not just for keeping contexts, but so that when passing a function away from our own processes to external processes those external processes cannot tinker with this and super. The external processes may store the functions inside objects, and we may deem it important that the passed functions act upon the context we specify instead of those storage containers. This is one thing arrow functions solve regarding this and super (more on that below). But, if "func.bind().toMethod(...) should throw", then how does one bind both this and HomeObject in order to guarantee that some external process doesn't modify one or the other (or for some other reason)?

Thus, if let func = function() {}, it might make sense for the following to be true:

  • func.bind(...) does not throw
  • func. ​toMethod​ (...) does not throw
  • func.bind(...).toMethod(...) does not throw
  • func. ​toMethod​ (...). ​bind​ (...) does not throw

​and errors should be thrown only when bind or toMethod are called on something that is already binded or toMethoded, respectively: ​

  • func.bind(...). ​bind​ (...) ​throws​
  • func. ​toMethod​ (...) ​.toMethod​ ​ ​(...) ​throws​
  • func.​toMethod​(...)​. ​bind​ ​​​(...) ​.bind(...)​ ​throws
  • func.​toMethod​(...)​.​bind​​​​(...)​. ​toMethod​ (...)​ ​throws
  • func.​ ​bind​ ​(...)​.​ ​toMethod​ ​​​​(...)​. ​toMethod​ (...)​ ​throws​
  • func.​ ​bind​ ​(...)​.​ ​toMethod​ ​​​​(...)​. ​bind​ (...)​ ​throws​

​It would also be important to consider that passing this.func.bind​(...) to an external process still leaves super up for modification, and vice versa if passing this.func.toMethod(...).

If someone has done this.func.bind() and has no idea what to pass to a following .toMethod call in order to prevent external processes from tinkering with it (which will be the case most of the time because requiring the user to lookup where on the prototype chain a method is located is requiring way too much work of them in order to achieve a simple outcome), then maybe there can be a new .lock() method that simply returns a new function bind()ed and toMethod()ed to whatever this and super are relative to the object passed to lock. For example,

$('.some-el').on('click', this.func.lock(this)) // binds both `this` and
`HomeObject`

would be essentially equivalent to

$('.some-el').on('click', this.func.bind(this).toMethod( findHome(this,
'func', this.func) ))

where findHome would be a necessary helper to have laying around, which proves that toMethod is not ideal for average cases, only for people involved in meta-programming.

The "issues with toMethod" in your mixin-proposal.pdf seem like implementation details that don't need to surface to the end user of the language. For example, consider the issues of toMethod

  • Requir[ing] copying
  • Hav[ing] the "deep clone" problem

I don't think copying or cloning is necessary. Internally, the engine could use a proxy that simply passes to the wrapped internal function this, HomeObject, or both, depending on which are bound, otherwise the engine falls back to contexts based on this and where in the prototype chain from this that super is being used (that algo doesn't seem expensive since the LookUp algo already finds the prototype (HomeObject) where a property lives, assuming super to be dynamic).

Internally, the proxy returned from func.bind(...) would contain the reference to the this argument, and leave super to the lookup algo starting lookup at that this context. Internally, the proxy returned from func.toMethod(...) would contain the HomeObject reference to pass into function calls of the wrapped function. The things returned are proxies, so property lookup could be forwarded without having to worry about cloning. We could call .toMethod() or .bind() on either of those two proxies, respectively, in which case the proxies record the second HomeObject or this arguments, respectively. Any more calls and an error can be thrown.

Implemented along those lines, there doesn't need to be any copying or cloning. (Maybe I'm missing why that is required, but it doesn't seem to be required the way I'm imagining it).

To make .bind() and .toMethod() not seem like magic (because currently .bind() returns a function who's direct prototype is Function.prototype, which is somewhat like magic and makes it foggy how the new function is related to the old one), there could be a new FunctionProxy class (it might extend from Function, or Proxy and Function if there were multiple inheritance). It would not be possible to get a reference to the original function from the FunctionProxy so that unauthorized manipulation of this and super can be guaranteed when that is what is wanted.

So, the following snippets would be effectively the same:

let f = function() {console.log(this, super)}
f = new FunctionProxy()
f.bind(this)
f.toMethod(findHome(this, 'nameOfMethodWeAreIn', this.nameOfMethodWeAreIn))
let f = function() {console.log(this, super)}
f = f.bind(this) // returns a FunctionProxy
f = f.toMethod(findHome(this, 'nameOfMethodWeAreIn',
this.nameOfMethodWeAreIn)) // returns the *same* FunctionProxy.
let f = function() {console.log(this, super)}
f = f.bind(this).toMethod(findHome(this, 'nameOfMethodWeAreIn',
this.nameOfMethodWeAreIn))
let f = function() {console.log(this, super)}
f = f.lock(this) // The lock method finds the home of `f` relative to
`this` if any, otherwise sets HomeObject to `this`, or similar.

The combination of bind with toMethod (or a new lock method) in those examples shows how to achieve essentially the same as with arrow functions, so

let f = function() {console.log(this,
super)}.bind(this).toMethod(findHome(this, 'nameOfMethodWeAreIn',
this.nameOfMethodWeAreIn))

is effectively equivalent to the following using arrow functions (disregarding arguments and new.target):

let f = () => {console.log(this, super)} // <-- yes!

The mixin-proposal.pdf mentions that

Programmers don't need to think about or even be aware of the internals

of super.

Programmers are currently aware of needing to sometimes bind functions passed to other libraries, or to bind functions in order to make them operate on certain objects as if they were methods of those objects, etc.

Well, considering developers know this about .bind(), the nice thing about passing obj.func.bind(obj) to an external process is that the external process can call the passed function and that call will be equivalent to calling obj.func() in the original process, and therefore a dynamic super will still work as expected without programmers having to actually use toMethod because lookup for super would start at the prototype chain leaf which is referenced by the this that was bound. This concept is backwards compatible with pre-ES6 .bind() usage, and so people coming from ES5 and using .bind() in ES8 or ES9 (or wherever dynamic super lands) will not be surprised, and it will be intuitive.

Note that passing the bound function in the last paragraph still leaves the function susceptible to being called with a tinkered HomeObject.

toMethod will be mostly useful for interesting cases where people are implementing frameworks or tools and need to have control over classes or inheritance schemes are implemented, or something meta-programming like that. But, people simply defining classes will never need to worry about super internals even if it is dynamic and if toMethod exists.

A dynamic super means that super would be allowed in any function definition, not just object-initializer methods or class definition methods, which makes it easy to Object.assign() things, copy methods ad-hoc, and basically enjoy ES5-like techniques. ES6 classes and object-initializers should be limited to being syntactical shortcuts as much as possible, and not impose limitations on developers that cause developers to have to abandon pre-ES6 techniques (copying methods from one prototype to another for example) just because something like super is unfortunately static.

This destroys the pre-ES6 flexibility that developers had in

manipulating prototypes because now they cannot apply the same techniques with ES6 classes.

It may be tempting for someone to refute this and say "well, then just write your classes the ES5 way". That's fine and dandy, but then it means my tools for manipulating ES5 classes will be highly incompatible with ES6 classes (which are supposed to be "syntax sugar").

This is bad because it causes fragmentation in the ecosystem. Some libraries will work only with ES5 classes, others with ES6 classes, and some libraries will be incompatible with each other because the paradigm has forked into two directions rather than ES6 being a pure extension of ES5.

If super is static, then should I care about prototypes anymore? Maybe

I shouldn't care about them anymore because they can't be manipulated without super getting in the way. Should I stop thinking about JavaScript as a prototypal language and abandon my efforts at manipulating prototypes in order to create a multiple-inheritance scheme? Should I just write ES6 classes and not think about prototypes and pretend JavaScript was never a prototypal language?

That is not what I want, and I bet highly that many of us here don't want that, but that is the direction that a static super is taking us, and the mixin {} operator idea is like a rocket propelled explosive going down that wrong path in the paradigm fork that I mentioned just moments ago, ready to cause fragmentation, as in "oh, I'll use Object.assign on my ES5 classes, and mixin {} on my ES6 classes" or "darn, I wanted to use ES6 classes because I like the syntax, but I can't because static super is in the way. So much for syntax sugar.". (plus, ES6 constructors not being callable is also in the way in my case, as I can't simply call them on another this which I really want to do, but I'll save that for another day, and will just use my Class().extends() API to mimic ES6 syntax in defining all my classes, but then I will actually be able to implement my multiple()-inheritance helper without needing eval and unfortunately without using ES6 classes -- maybe I'll use ES9 or ES10 classes when super is dynamic...)

There's the current path (static super) and the path that is in-line with ES5 (dynamic super and ES5-based tools like Object.assign behaving intuitively).

(Note, some may want a function mixed into somewhere to have it's original HomeObject, and that is what toMethod can be useful for. Everyone can be satisfied.)

The world will be much better this way, people will have more freedom to implement interesting things, and it will be more intuitive (_.extend will keep working in cases where methods use super!); it's better for the world to have this flexibility. Most developers will just use ES6 classes without worrying about super internals, even if toMethod exists and/or super is dynamic. toMethod usage will be rare, but it will be powerful when it is needed.

toMethod or a dynamic super will not be a footgun because most people will never need to know or work with those details, plus with super being dynamic it means JavaScript can behave the way we already expect from two decades worth of using the keyword this, which I think is A REALLY GOOD THING.

It would mean that this and super can be used anywhere and we wouldn't have to distinguish between functions and methods-defined-by-object-initializer-shorthand (paradigm fragmentation). The distinction is not intuitive. We can merge the paradigm fragmentation by simply making super dynamic along with introducing the tool to configure HomeObject on as-needed basis for the rare cases (and even if it were a footgun, it's need is rare, and people who wield the footgun will probably convert the footgun into a powerful tool of mass benefit).

At the end of the day, it would be excellent for the following to work, and is what would be intuitive, without requiring a new operator:

let o = {
  __proto__: other,
  func() {super.func()}
}
let o = {
  __proto__: other,
  func: function func() {super.func()} // this should literally be 100%
equivalent to the previous example
}
let o = {
  __proto__: other
}
Object.assign(o, {
  func: function func() {super.func()} // and this should end up being
exactly equivalent too.
})

Let's make mixing in methods as simple as Object.assign(obj, {...}) Let's make it simpler with mixin {...}

I'm just re-iterating, but Object.assign() is all we need, and new operators are needed because they solidify the provably-unwanted and unintuitive static super. Adding more to the language on top of static super may cause more and more divergence between ES5 (things like Object.assign -- I know it came out in ES6, but _.extend was out before ES6) and ES6 (class-definition methods, object-initializer methods), and fragment the language into two paths. A dynamic super solves the problem, and it can be well documented so that developers will know what to expect just like they already have with two decades of this. A dynamic super is perfectly in line with the dynamic nature this (performance considerations aside).

If there really is a performance problem with a dynamic super, let's just solve that instead of creating separate syntaxes that are incompatible with the JavaScript we already know. A dynamic super is also forwards and ahead of what super is in other languages. JavaScript doesn't need to go in that direction, it needs to go furthermore forwards.

Also keep in mind that a dynamic super will prevent surprises to developers who are beginning to learn JavaScript in the post-ES6 era.

We should strive for a dynamic super (which would be completely backwards compatible with how it currently works) along with something like toMethod.


Does anyone mind pointing out the specific unwanted overhead costs of a dynamic super?

# Michał Wadas (8 years ago)

fn.bind().bind() can't throw because of backward compatibility.

# Andrea Giammarchi (8 years ago)

I'm just re-iterating, but Object.assign() is all we need

losing getters and setters where super is also allowed is all we don't need as well, not sure why keep ignoring the fact Object.assign has undesired side-effects.

# /#!/JoePea (8 years ago)

Andrea,

losing getters and setters where super is also allowed is all we don't

need as well, not sure why keep ignoring the fact Object.assign has undesired side-effects.

Right, but that's well-explained (for example, in the MDN docs). It is also easy to use getOwnPropertyNames or getOwnPropertyDescriptors. But, even if we copy descriptors, the static nature of super will still get in the way when copying a getter/setter descriptor from one object to another.

So, my argument is the same: a dynamic super makes Object.assign as well as any other methods of method borrowing work as expected (talking strictly in context of how super would work, ignoring other side-effects not relating to super).

# /#!/JoePea (8 years ago)

fn.bind().bind() can't throw because of backward compatibility.

Well, that's fine with me. The important thing to consider would be those other things I listed that shouldn't throw. These ones:

  • func.bind(...) does not throw
  • func.​toMethod​(...) does not throw
  • func.bind(...).toMethod(...) does not throw
  • func.​toMethod​(...).​bind​(...) does not throw

As for the throwing cases, I don't really care if they throw. They could also just silently fail. What I'm pointing out here is that it should be possible to bind boththis and HomeObject and not be restricted just because one or the other was already bound.

# /#!/JoePea (8 years ago)

Alan, after considering the March 25th Declarative Alternative to toMethod conversation, it really seems like a dynamic super would solve the problems mentioned. The extra syntaxes,

let mixins = obj => obj mixin {...}; mixins(target);

mixin TextSupport {...}
class MyComponent extends EmberComponent with TextSupport {...}
function (super) foo (x, y) { ...super ...x ...y }
//etc

just seem like ways to work around the static super problem, introduce a bunch of new syntax when it is completely not needed, and as mentioned in that conversation:

YK: want to avoid "harsh end of life" outcome for ES5-ish libraries.

which depicts the very fragmentation between ES5 and ES6 that I mentioned previously, which will cause a paradigm fork where some libraries will be targeted at ES5 features or ES6 features but not both when ideally the libraries should work with both feature sets. It would be great for the progress of the JavaScript language to be linear rather than forked (f.e. concise methods and class methods should be just functions with dynamic supers that can be manipulated just as we're used to in the pre-ES6 era).

If super were dynamic, then users of widely-adopted libraries like Backbone would have huge benefits:

let NewClass = SomeBackboneClass.extend({
  someMethod() {
    // ...
    super.someMethod() // this would work!! It would be awesome for
existing code bases!!
  }
})

That will currently fail for the same reason as why Object.assign fails: because HomeObject is static.

Would you or someone please convince me that a dynamic super would be overhead-costly and performance-killing enough to have it be static?

# /#!/JoePea (7 years ago)

Here's a new real-world example showing people are naturally expecting ES6 super to be dynamic when they copy methods from objects to other objects: stackoverflow.com/questions/46306598.

It makes sense to assume it would work this way considering how dynamic pre-ES6 is!