The `super` keyword doesn't work as it should?
/#!/JoePea
joe at trusktr.io
Mon Aug 1 07:20:10 UTC 2016
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 `bind`ed or `toMethod`ed, 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,
```js
$('.some-el').on('click', this.func.lock(this)) // binds both `this` and
`HomeObject`
```
would be essentially equivalent to
```js
$('.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](
https://github.com/tc39/tc39-notes/blob/master/es6/2015-03/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:
```js
let f = function() {console.log(this, super)}
f = new FunctionProxy()
f.bind(this)
f.toMethod(findHome(this, 'nameOfMethodWeAreIn', this.nameOfMethodWeAreIn))
```
```js
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.
```
```js
let f = function() {console.log(this, super)}
f = f.bind(this).toMethod(findHome(this, 'nameOfMethodWeAreIn',
this.nameOfMethodWeAreIn))
```
```js
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
```js
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`):
```js
let f = () => {console.log(this, super)} // <-- yes!
```
The [mixin-proposal.pdf](
https://github.com/tc39/tc39-notes/blob/master/es6/2015-03/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](
https://www.npmjs.com/package/lowclass) 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:
```js
let o = {
__proto__: other,
func() {super.func()}
}
```
```js
let o = {
__proto__: other,
func: function func() {super.func()} // this should literally be 100%
equivalent to the previous example
}
```
```js
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`?
*/#!/*JoePea
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20160801/aed5638c/attachment-0001.html>
More information about the es-discuss
mailing list