The `super` keyword doesn't work as it should?
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.
/#!/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 onthis
"? 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 class
es 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
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.
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
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.
An alternative would be to consider the object where the method key was found as the home object of the method.
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.
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
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.
In this example,
super.method()
will work fine, anda.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
.method
s 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) {}
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.
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
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.
@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?
/#!/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.
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.
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
.
On Jul 24, 2016, at 7:04 PM, /#!/JoePea <joe at trusktr.io> wrote:
What if there was also something like
Function.prototype.bind
likeFunction.prototype.with
, sosomeFunc.with(homeObject)
returns a new function who's [[HomeObject]] is the specifiedhomeObject
. It would be possible to dosomeFunc.with(...).bind(...)
to configure both the home object andthis
.
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:
- There are indeed use cases for dynamically configuring the HomeObject binding but they are quite specialized.
- All of the solution that have been proposed are confusing or error prone.
- So far, the actual user demand for such a feature is small.
- We aren’t going to do anything until there is significant real world feedback saying that it really is needed.
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.
- 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 { ... }
class
OtherClass
{ ... }
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'
class
OtherClass
{ ... }
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 HomeObject
s 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 HomeObject
argument, 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.
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?
For reference, here's the implementation of multiple()
where multiple
constructors calls are possible, using a callSuperConstructor
helper:
gist.github.com/trusktr/05b9c763ac70d7086fe3a08c2c4fb4bf
Could you not make multiple
be a function that returns a Proxy, rather
than achieving multiple inheritance by copying properties?
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
MultiClass
of
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 usingnew ctor()
, then copying the properties from the created objects onto thethis
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 withReflect.construct
. Hmmm...
- Maybe I can achieve this with
- 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)?
Allen, I read your linked documents. After thinking about it more, I believe it would be ideal to have both:
- 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()
, sotoMethod
clashes with that. Also, we're not really assigning a method onto an object withtoMethod
, so it seems awkward. We're setting the HomeObject, that's what we're doing. - 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 throwfunc. toMethod (...)
does not throwfunc.bind(...).toMethod(...)
does not throwfunc. toMethod (...). bind (...)
does not throw
and errors should be thrown only when bind
or toMethod
are called on
something that is already bind
ed or toMethod
ed, respectively:
func.bind(...). bind (...)
throwsfunc. toMethod (...) .toMethod (...)
throwsfunc.toMethod(...). bind (...) .bind(...)
throwsfunc.toMethod(...).bind(...). toMethod (...)
throwsfunc. bind (...). toMethod (...). toMethod (...)
throwsfunc. 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 withmixin {...}
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
?
fn.bind().bind() can't throw because of backward compatibility.
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.
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
).
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 throwfunc.toMethod(...)
does not throwfunc.bind(...).toMethod(...)
does not throwfunc.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.
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
super
s 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?
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!
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 onthis
"? 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?