Why are object initializer methods not usable as constructors?
IIRC methods shorthand are the only where super
is allowed, at least in
Chrome and Firefox.
Accordingly, if you use them as classes, the super won't point at the one you'd expect, it would point to the object hosting those classes as properties.
class Animal { setName(name) { this.name = name; } }
class Cat extends Animal { constructor() { super(); super.setName('cat'); }}
let classes = {
__proto__: {setName: 'nope'}
Cat
};
new classes.Cat(); // ?
I guess you cannot have the best from both worlds.
The most common case is the “methods” are designed to be used as constructors. This has always be recognized for built-in methods in the ECMAScript specification which says: "Built-in function objects that are not identified [in this specification] as constructors do not implement the [[Construct]] internal method unless otherwise specified in the description of a particular function.”. When “concise methods” where add as part of Object Initializers and Class Definitinos this default was applied to them.
You can still define constructible properties for Object Initializers, just not by using concise method syntax:
let classes = {
Cat: function() {},
Dog: function() {},
Bird: class {}
};
let [c, d, b] = [new classes.Cat, new classes.Dog, new classes.Bird];
What's the good reason that object-initializer methods can't be constructors though? I mean, I get that "that's what spec says", but what's the actual good reason?
The problem here would be solved with a dynamic super.
I feel that ES6 diverges from ES5 in backwards-incompatible ways. Some
people say ES6 classes are "syntax sugar" over the ES5 constructor pattern
classes, but, not really, since they aren't compatible in the same
places. ES6 classes and object-initializer methods are not
backwards-incompatible in a bad way because they cannot be treated the same
as we are used to in ES5 (f.e. copying methods with underscore's
_.extend()
doesn't work because of static HomeObjects), and that is
backwards-incompatible with previous concepts, ideas, and methodologies
that we love JavaScript for.
Some old code might do something like
_.extend(SomeClass.prototype, OtherClass.prototype)
where SomeClass
and OtherClass
are ES5 constructor-pattern classes. If
the classes are updated to be ES6 classes, then that code may fail because
of the static HomeObject
properties, and that is backwards-incompatible
with the paradigms and patterns of ES5.
A dynamic super would remedy this problem.
For now, I am contemplating using a custom and dynamic
Object.prototype.super
getter in all apps where my code runs so that my
ES6 classes remain as flexible as in ES5. The overhead associated with a
custom Object.prototype.super
would only be due to the fact that it is
not possible for us to hook into the native property lookup algorithm,
otherwise the overhead of finding a HomeObject would be completely
negligible because the native property lookup algorithm already finds the
HomeObject where a property or method is located and therefore it would
cost next to nothing to use the found HomeObject on a method call by
passing HomeObject as an argument in the native side of the JS engine.
IIRC methods shorthand are the only where super is allowed, at least in
Chrome and Firefox.
If super
were dynamic, then super
could be allowed anywhere, not just
in Classes or object-initializer shorthand methods.
For what it's worth, Object.assign
is a concept promoted into the
language by pre-ES6 ideas, like underscore's _.extend
and others. So, the
mere fact that Object.assign
will fail to produce expected results when
copying something from an ES6 prototype that uses super
means that the
direction of the ES6+ language is backwards-incompatible with pre-ES6
JavaScript and that ES6+ features are incompatible with themselves.
If something like Function.prototype.toMethod
made it into spec but not a
dynamic super
, then solution would be half-complete: tools like
Object.assign
would also need to start using toMethod
internally so
that results would be intuitive.
Arguably, the best solution would be for super
to be dynamic and for
Function.prototype.toMethod
to exist, following the same patterns as we
love about JavaScript's this
keyword. This would open up
meta-programming possibilities, for example it would then be easy to write
a prototype-based implementation of multiple inheritance whereby a
single prototype chain is created based on the multiple classes we wish to
extend from (this is in constrast to a Proxy
-based implementation).
Claude Pache mentioned that he likes for method borrowing to maintain the
same HomeObject,
but in my opinion that does not stay inline intuitive expectations coming
from pre-ES6 with a dynamic this
: "I intuitively expect super to be
relative to the prototype chain of the current object where a method is
called".
If you are technical enough to understand why method borrowing fails due to
a static super
, then you'd also be technical enough to understand why it
works the dynamic way if that ever became reality. Plus, if
Function.prototype.toMethod
is brought into spec, then you'd also be
technical enough to borrow the method and assign the original HomeObject
that you wish.
I hope you're not using Object.assign
to copy anything different from
basic setup-like or arguments objects: if I were you I would use
Object.defineProperties(targetProto, Object.getOwnPropertyDescriptors(superProto))
or it gonna be "bad time".
I think they answered already on the dynamic super, not sure keep raising the issue would help :-(
Best
On Wed, Jul 27, 2016 at 11:44 AM, /#!/JoePea <joe at trusktr.io> wrote:
What's the good reason that object-initializer methods can't be constructors though? I mean, I get that "that's what spec says", but what's the actual good reason?
Because they're methods, not functions. The distinction between the two was merely semantic in ES5, but now it's mechanical, due to super(); constructing something intended as a method would make super() behave in confusing and unintuitive ways, so methods just don't have a constructor any more.
There are several very similar ways you can write your example that do achieve what you want. As Allen said, you can just use the non-concise syntax, explicitly typing "function" (or better, "class") for each of the values. This is only a few characters more and achieves exactly what you want.
It's been explained to you already in previous threads why super() is designed the way it is, and how a dynamic super() would add significant cost to some situations. Making this one niche use-case ("I want to define several constructor-only classes inline in an object initializer") require a few characters less is not a sufficiently worthwhile benefit for the cost. Just type the few extra characters (exactly what you would have typed in ES5, so it's not even a new imposition), and you'll be fine.
The distinction between the two was merely semantic in ES5, but now it's
mechanical, due to super(); constructing something intended as a method would make super() behave in confusing and unintuitive ways, so methods just don't have a constructor any more.
So, if super
were dynamic, then it would be no problem.
It's been explained to you already in previous threads why super() is
designed the way it is, and how a dynamic super() would add significant cost to some situations
Those "some situations" haven't been listed yet (or I don't know where they are listed). Do you know any? As far as I can tell, a dynamic super would perform just fine:
- For constructor calls,
HomeObject
can just the.prototype
property of the function whennew.target
is the same as the function being constructed. That's simple. - If
new.target
is not the current constructed function, then the current function was found on the prototype chain ofObject.getPrototypeOf(new.target)
(i.e. tje.constructor
property was found on someHomeObject
in the prototype chain) and then that function is called with the foundHomeObject
. This seems like a simple addition to the property lookup algorithm. - Functions called as methods simply have
HomeObject
passed as the prototype (HomeObject) where they were found. This seems like a simple addition to the property lookup algorithm. - What else?
Based on those ideas from my limited knowledge on thetopic, a dynamic
super
doesn't seem to "costly".
Suppose I write
obj.foo()
Then, in ES5, there is already going to be a property lookup algorithm to
find foo
on the prototype chain of obj
. Therefore, when the
propertyfoo
is found on a prototype (a HomeObject), it doesn't seem like
all that much extra cost to simply pass that found object by reference to
the foo
method call, since we already found it. That's not very costly.
I may be using the word "HomeObject" wrong, but I think you get what I mean.
Because they're methods, not functions. The distinction between the two was merely semantic in ES5, but now it's mechanical, due to super(); constructing something intended as a method would make super() behave in confusing and unintuitive ways, so methods just don't have a constructor any more.
You are correct for app authors, but wrong for library authors. A library author may easily like to make an object that contains ES5 constructors, and writing them like
let ctors = {
Foo() {},
Bar() {},
}
is simply simple.
For example, suppose a library author releases a Class
function for
defining classes, it could be used like this:
const Animal = Class({
constructor: function() {
console.log('new Animal')
}
})
but most JS authors who don't know about these pesky new JavaScript language internals might be inclined to write:
const Animal = Class({
constructor() {
console.log('new Animal')
}
})
If the Class
implementation returns that "constructor", then when
the user does new Animal
they will get an unexpected error, and that
is not ideal at all for a dynamic language like JavaScript.
What's even stranger is that the Class
implementation can wrap the
concise method with a [[Construct]]able function and call the concise
method with .call
or .apply
and it will work! But that is simply
just messy and ugly.
So why prevent it from working only sometimes? It would be much better
for it to just work all the time, and make restrictions only when
super
is present in the function body. In the above example, the
constructor() {}
concise method does not use the keyword super
, so
treating it like constructor: function constructor() {}
would be
much more ideal.
We shouldn't limit developer creativity for reasons that don't exist (I haven't heard of any compelling reasons so far).
The new language features cause failures in unexpected ways, and I really don't think the language should be designed in this less-intuitive way.
JavaScript pre-ES6 has a dynamic nature, but ES6 and newer features are less-so in that tradition.
Making this one niche use-case ("I want to define several constructor-only classes inline in an object initializer") require a few characters less is not a sufficiently worthwhile benefit for the cost.
Actually, no, I want to allow end-users of my library to pass in objects containing methods and properties, and I don't want the result to fail in unexpected ways, and I also don't want to write ugly and hacky code to make it work.
Just type the few extra characters (exactly what you would have typed in ES5, so it's not even a new imposition), and you'll be fine.
Like I said, it won't be me typing these things, it will be end users.
Yes I can disguise the problem, but if I for example were implementing
a Class
tool, I wouldn't like to wrap their non-constructable
methods in a proxy function just to make it work not only because it
is ugly, but because it will show things in the console that are more
difficult to debug.
For example, have you ever looked at classes made with Backbone? They are not so nice to inspect because Backbone wraps constructors and prototypes like an onion.
This language "feature" of concise methods that makes them not constructable forces library authors to write ugly code who's output is harder to inspect and debug by end developers. /#!/JoePea
You seem to be suggesting that ES6 should be making it easier for you to
reimplement a core ES6 language feature. If you want class
, can you not
use class
? That's what users who have access to ES6+ environments are
likely to do anyways.
It's also worth noting that someone could do constructor: () => {}
or
constructor: function *() {}
and new
ing them would fail the same way.
Methods not being constructors also makes them lighter-weight: They have no
prototype
property and associated object.
This isn't limiting developer creativity or freedom. If you want to create an object with constructor functions, you have at least two ways to do that:
class
:
let ctors = {
Foo: class { },
Bar: class { }
};
function
:
let ctors = {
Foo: function { },
Bar: function { }
};
but most JS authors who don't know about these pesky new JavaScript language internals might be inclined to write:
Then the library author warns them not to do that in the documentation (maybe even a really clear error message in the non-min build), or they figure it out really quickly when they do and it doesn't work. :-)
-- T.J. Crowder
/#!/JoePea
On Mon, May 15, 2017 at 6:16 PM, Jordan Harband <ljharb at gmail.com> wrote:
You seem to be suggesting that ES6 should be making it easier for you to reimplement a core ES6 language feature. If you want
class
, can you not
Yeah, sure, why not? Allow library authors to do awesome things like provide libraries for multiple inheritance that aren't hacks.
import Bar from './Bar'
import Baz from './Baz'
import multiple from 'multiple-inheritance-library-by-some-author'
class Foo extends multiple(Bar, Baz) {}
use
class
? That's what users who have access to ES6+ environments are likely to do anyways.
ES6 classes don't have protected or private members, but an author's
Class
tool might.
Not everyone is writing ES6 in a shiny new app. There's large outdated
code bases. It'd be convenient for a tool like Class
to work on old
code, and not fail on new code.
It's also worth noting that someone could do
constructor: () => {}
orconstructor: function *() {}
andnew
ing them would fail the same way.
Yes, we can't prevent all the bad usages, but those are obviously not meant to work. Concise methods aren't obviously going to fail, especially considering that they are not really called "concise methods" by most people, but more like "shorthands", and by that terminology the layman is going to expect them to work. Arrow function and generatos are very explicitly different, and you have to actually know what they are in order to use them.
There's always going to be some way to make some library fail, but
that doesn't mean we should add more ways to make code fail when we
can avoid it. Arrow functions and generators are necessary for a
purpose. However, making concise methods that don't use the keyword
super
non-constructable doesn't really have any great purpose, it
only makes certain code fail in needless ways...
But if you're introducing a new Class library, your writing new code... You could almost as really setup a build chain (Babel) to support class syntax, if you can't already target sorted browsers.
I feel like recent changes in the language (ES6+) introduce new features that don't have the flexibility as the pre ES6 language that we're used to.
For example,
super
is static and inflexible which is not inline with howthis
works.Object initializer methods are also limited. Suppose I want to define an object that contains various constructors. I would be inclined to use object-initializer shortcuts:
let classes = { Cat() {}, Dog() {}, Bird() {} }
but this doesn't work:
new classes.Dog() // Uncaught TypeError: classes.Dog is not a constructor
So, here's another failure of intuition (
super
being static was also not intuitive). We can fix this by writing:function Cat() {} function Dog() {} function Bird() {} let classes = { Cat, Dog, Bird }
but that's not as convenient. What's the reason why we shouldn't be able to do that? I feel like JavaScript is being restricted in undesirable ways. I love JavaScript because it has always been so flexible, and I would expect the new features to continue being flexible. That's what makes JavaScript great.